com.basetechnology.s0.agentserver.AgentInstance.java Source code

Java tutorial

Introduction

Here is the source code for com.basetechnology.s0.agentserver.AgentInstance.java

Source

/**
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 *     
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.basetechnology.s0.agentserver;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.apache.log4j.Logger;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import com.basetechnology.s0.agentserver.activities.AgentActivityNotification;
import com.basetechnology.s0.agentserver.activities.AgentActivityTriggerInputChanged;
import com.basetechnology.s0.agentserver.field.Field;
import com.basetechnology.s0.agentserver.goals.Goal;
import com.basetechnology.s0.agentserver.notification.MailNotification;
import com.basetechnology.s0.agentserver.notification.NotificationDefinition;
import com.basetechnology.s0.agentserver.notification.NotificationHistory;
import com.basetechnology.s0.agentserver.notification.NotificationInstance;
import com.basetechnology.s0.agentserver.notification.NotificationRecord;
import com.basetechnology.s0.agentserver.scheduler.AgentScheduler;
import com.basetechnology.s0.agentserver.script.intermediate.ExpressionNode;
import com.basetechnology.s0.agentserver.script.intermediate.MapTypeNode;
import com.basetechnology.s0.agentserver.script.intermediate.ObjectTypeNode;
import com.basetechnology.s0.agentserver.script.intermediate.ScriptNode;
import com.basetechnology.s0.agentserver.script.intermediate.Symbol;
import com.basetechnology.s0.agentserver.script.intermediate.SymbolException;
import com.basetechnology.s0.agentserver.script.intermediate.SymbolManager;
import com.basetechnology.s0.agentserver.script.intermediate.SymbolTable;
import com.basetechnology.s0.agentserver.script.intermediate.SymbolValues;
import com.basetechnology.s0.agentserver.script.intermediate.TypeNode;
import com.basetechnology.s0.agentserver.script.parser.ParserException;
import com.basetechnology.s0.agentserver.script.parser.ScriptParser;
import com.basetechnology.s0.agentserver.script.parser.tokenizer.TokenizerException;
import com.basetechnology.s0.agentserver.script.runtime.ExceptionInfo;
import com.basetechnology.s0.agentserver.script.runtime.ParsedScripts;
import com.basetechnology.s0.agentserver.script.runtime.ScriptRuntime;
import com.basetechnology.s0.agentserver.script.runtime.value.FieldValue;
import com.basetechnology.s0.agentserver.script.runtime.value.MapValue;
import com.basetechnology.s0.agentserver.script.runtime.value.NullValue;
import com.basetechnology.s0.agentserver.script.runtime.value.Value;
import com.basetechnology.s0.agentserver.util.DateUtils;
import com.basetechnology.s0.agentserver.util.JsonListMap;
import com.basetechnology.s0.agentserver.util.JsonUtils;
import com.basetechnology.s0.agentserver.util.ListMap;
import com.basetechnology.s0.agentserver.util.NameValue;

public class AgentInstance {
    static final Logger log = Logger.getLogger(AgentInstance.class);
    public static final int DEFAULT_MAX_INSTANCES = 1000;
    public static final int DEFAULT_LIMIT_INSTANCE_STATES_STORED = 25;
    public static final int DEFAULT_MAXIMUM_LIMIT_INSTANCE_STATES_STORED = 1000;
    public static final int DEFAULT_LIMIT_INSTANCE_STATES_RETURNED = 10;
    public static final int DEFAULT_MAXIMUM_LIMIT_INSTANCE_STATES_RETURNED = 1000;
    public static final boolean DEFAULT_PUBLIC_OUTPUT = false;
    public AgentServer agentServer;
    public long timeInstantiated;
    public long timeUpdated;
    public User user;
    public String name;
    public String description;
    public AgentDefinition agentDefinition;
    public SymbolValues parameterValues;
    public Map<String, String> scriptStatus;
    public Map<String, Long> scriptStartTime;
    public Map<String, Value> scriptReturnValue;
    public Map<String, Long> scriptEndTime;
    public Map<String, AgentTimerStatus> timerStatus;
    public Map<String, AgentConditionStatus> conditionStatus;
    public String triggerIntervalExpression;
    public String reportingIntervalExpression;
    public Boolean publicOutput;
    public int limitInstanceStatesStored;
    public Boolean enabled;
    public boolean pendingSuspended;
    public boolean busy;
    public long lastInputsChanged;
    public long lastTriggerReady;
    public long lastTriggered;
    public List<AgentState> state;
    public ScriptRuntime scriptRuntime;
    public SymbolManager symbolManager;
    public Map<String, SymbolValues> categorySymbolValues;
    public ParsedScripts parsedScripts;
    public boolean scheduledInit;
    public boolean ranInit;
    public List<AgentInstance> dependentInstances;
    public Map<DataSourceReference, AgentInstance> dataSourceInstances;
    public OutputHistory outputHistory;
    public static final int DEFAULT_OUTPUT_COUNT = 10;
    public int defaultOutputCount = DEFAULT_OUTPUT_COUNT;
    public static final int DEFAULT_OUTPUT_LIMIT = 20;
    public int outputLimit = DEFAULT_OUTPUT_LIMIT;
    public boolean update;
    public List<ExceptionInfo> exceptionHistory;
    public ListMap<String, NotificationInstance> notifications;
    public NotificationHistory notificationHistory;
    public long lastDismissedExceptionTime;
    public boolean suppressEmail;
    public boolean autoCreated = false;
    public boolean check;
    public boolean deleted;

    public static int autoNameCounter = 0;

    public AgentInstance() {
        // Nothing needed
    }

    public AgentInstance(AgentDefinition agentDefinition) throws AgentServerException {
        this(User.noUser, agentDefinition, null, false);
    }

    public AgentInstance(AgentDefinition agentDefinition, boolean check) throws AgentServerException {
        this(User.noUser, agentDefinition, null, check);
    }

    public AgentInstance(User user, AgentDefinition agentDefinition) throws AgentServerException {
        this(user, agentDefinition, null, false);
    }

    public AgentInstance(User user, AgentDefinition agentDefinition, boolean check) throws AgentServerException {
        this(user, agentDefinition, null, check);
    }

    public AgentInstance(User user, AgentDefinition agentDefinition, SymbolValues parameterValues)
            throws AgentServerException {
        this(user, agentDefinition, null, null, parameterValues, null, null, false, -1, agentDefinition.enabled, -1,
                -1, null, false, false);
    }

    public AgentInstance(User user, AgentDefinition agentDefinition, SymbolValues parameterValues, boolean check)
            throws AgentServerException {
        this(user, agentDefinition, null, null, parameterValues, null, null, false, -1, agentDefinition.enabled, -1,
                -1, null, false, check);
    }

    public AgentInstance(User user, AgentDefinition agentDefinition, String name, String description,
            SymbolValues parameterValues, String triggerIntervalExpression, String reportingIntervalExpression,
            Boolean publicOutput, int limitInstanceStatesStored, Boolean enabled, long timeInstantiated,
            long timeUpdated, List<AgentState> state, boolean update, boolean check) throws AgentServerException {
        this.check = check;
        this.update = update;
        this.timeInstantiated = timeInstantiated > 0 ? timeInstantiated : System.currentTimeMillis();
        this.timeUpdated = timeUpdated > 0 ? timeUpdated : 0;
        this.user = user == null ? User.noUser : user;
        this.agentDefinition = agentDefinition;
        this.agentServer = agentDefinition.agentServer;
        this.name = name == null
                ? (check ? "check__" + agentDefinition.name : agentDefinition.name + "_" + ++autoNameCounter)
                : name;
        this.description = description;
        this.parameterValues = parameterValues == null && !update ? new SymbolValues("parameters")
                : parameterValues;
        this.dependentInstances = new ArrayList<AgentInstance>();
        this.dataSourceInstances = new HashMap<DataSourceReference, AgentInstance>();
        if (!update || check)
            initCategorySymbolValues(parameterValues);
        this.scriptStatus = new HashMap<String, String>();
        this.scriptStartTime = new HashMap<String, Long>();
        this.scriptReturnValue = new HashMap<String, Value>();
        this.scriptEndTime = new HashMap<String, Long>();
        this.timerStatus = new HashMap<String, AgentTimerStatus>();
        this.conditionStatus = new HashMap<String, AgentConditionStatus>();
        this.triggerIntervalExpression = triggerIntervalExpression == null
                || triggerIntervalExpression.trim().length() == 0 ? agentDefinition.triggerIntervalExpression
                        : triggerIntervalExpression;
        this.reportingIntervalExpression = reportingIntervalExpression == null
                || reportingIntervalExpression.trim().length() == 0 ? agentDefinition.reportingIntervalExpression
                        : reportingIntervalExpression;
        this.publicOutput = publicOutput;
        this.limitInstanceStatesStored = limitInstanceStatesStored >= 0 ? limitInstanceStatesStored
                : update ? limitInstanceStatesStored : agentServer.config.getDefaultLimitInstanceStatesStored();
        this.lastInputsChanged = 0;
        this.lastTriggerReady = 0;
        this.lastTriggered = 0;
        this.scriptRuntime = new ScriptRuntime(this);
        this.parsedScripts = new ParsedScripts();
        this.outputHistory = new OutputHistory();
        this.exceptionHistory = new ArrayList<ExceptionInfo>();
        // TODO: How to default this:
        this.lastDismissedExceptionTime = 0;
        this.notifications = new ListMap<String, NotificationInstance>();
        this.notificationHistory = new NotificationHistory();
        this.pendingSuspended = false;
        this.suppressEmail = false;
        this.deleted = false;

        if (!update && !check)
            this.enabled = false;

        // Build the symbol manager
        if (!update || check)
            buildSymbols();

        // Pre-parse scripts (especially functions)
        if (!update && !check)
            parseScripts();

        // Set initial state for instance.

        // If state was specified, restore it
        // TODO Whether to do this before or after setting up data sources?
        if (!update && !check)
            setState(state, update);

        // Initialize status for conditions and timers
        if (!update && !check) {
            initializeConditionStatus();
            initializeTimerStatus();
        }

        this.busy = false;
        this.ranInit = false;

        if (!update && !check && enabled != null && enabled)
            enable();
        log.info("Enabled for " + this.name + ": " + enabled);
    }

    public boolean equals(AgentDefinition otherAgentDefinition, SymbolValues otherParameterValues) {
        return (agentDefinition == otherAgentDefinition && parameterValues.equals(otherParameterValues));
    }

    protected void initCategorySymbolValues(SymbolValues parameterValues) {
        categorySymbolValues = new HashMap<String, SymbolValues>();
        categorySymbolValues.put("parameters",
                parameterValues == null ? new SymbolValues("parameters") : parameterValues);
        categorySymbolValues.put("inputs", new SymbolValues("inputs"));
        categorySymbolValues.put("events", new SymbolValues("events"));
        categorySymbolValues.put("scratchpad", new SymbolValues("scratchpad"));
        categorySymbolValues.put("memory", new SymbolValues("memory"));
        categorySymbolValues.put("goals", new SymbolValues("goals"));
        categorySymbolValues.put("notifications", new SymbolValues("notifications"));
        categorySymbolValues.put("outputs", new SymbolValues("outputs"));
    }

    public void captureDataSourceOutputValues() throws AgentServerException {
        if (agentDefinition.inputs != null) {
            for (DataSourceReference dataSourceReference : agentDefinition.inputs) {
                // Get the instance for that data source reference
                AgentInstance dataSourceInstance = dataSourceInstances.get(dataSourceReference);

                // Create a map object with a key for each output field of the data source
                MapValue map = new MapValue(ObjectTypeNode.one, null);

                for (Field field : dataSourceReference.dataSource.outputs) {
                    // Get the raw field name for the data source output field
                    String fieldName = field.symbol.name;

                    // Get the field value from the data source instance
                    Value fieldValue = dataSourceInstance.getOutput(fieldName);

                    // Make a deep copy of it
                    Value fieldValueCopy = fieldValue.clone();

                    // Add the value copy to the map
                    map.put(fieldName, fieldValueCopy);
                }

                // Store the value as value of input data source name
                putInput(dataSourceReference.name, map);
            }
            log.info("Captured input values for instance " + name + ": "
                    + categorySymbolValues.get("inputs").toJson());
        }
    }

    public void enable() throws AgentServerException {
        enabled = true;
        captureState();

        // Queue the 'init' script to run if it hasn't already
        if (!scheduledInit) {
            scheduledInit = true;
            log.info("Scheduling 'init' for instance '" + name + "'");
            AgentScheduler.scheduleInit(this);

            // If no init script, we'e done "starting"
            if (!agentDefinition.scripts.containsKey("init"))
                ranInit = true;

        } else {
            log.info("Skipped scheduling 'init' for instance '" + name
                    + "' since it has already been performed before agent instance was enabled");

            // Now schedule all timers and conditions for this agent
            AgentScheduler.scheduleTimersAndConditions(this);
        }
    }

    public void disable() throws AgentServerException {
        if (enabled) {
            captureState();
            enabled = false;
        }
    }

    public void delete() throws AgentServerException {
        this.deleted = true;
        disable();
    }

    public void buildSymbols() throws SymbolException {
        // Re-build symbol manager tables
        symbolManager = new SymbolManager();

        // But definition may not be complete yet
        if (agentDefinition != null) {
            // Add parameters
            if (agentDefinition != null && agentDefinition.parameters != null)
                for (Field field : agentDefinition.parameters)
                    symbolManager.put("parameters", field.symbol.name, field.symbol.type);

            // Add inputs
            if (agentDefinition.inputs != null)
                for (DataSourceReference input : agentDefinition.inputs)
                    symbolManager.put("inputs", input.name, MapTypeNode.one);

            // Add scratchpad
            if (agentDefinition.scratchpad != null)
                for (Field field : agentDefinition.scratchpad)
                    symbolManager.put("scratchpad", field.symbol.name, field.symbol.type);

            // Add memory
            if (agentDefinition.memory != null)
                for (Field field : agentDefinition.memory)
                    symbolManager.put("memory", field.symbol.name, field.symbol.type);

            // Add goals
            if (agentDefinition.goals != null)
                for (Goal goal : agentDefinition.goals)
                    symbolManager.put("goals", goal.name, ObjectTypeNode.one);

            // Add notifications
            if (agentDefinition.notifications != null)
                for (NameValue<NotificationDefinition> nameValue : agentDefinition.notifications)
                    symbolManager.put("notifications", nameValue.name, MapTypeNode.one);

            // Add outputs
            if (agentDefinition.outputs != null)
                for (Field field : agentDefinition.outputs)
                    symbolManager.put("outputs", field.symbol.name, field.symbol.type);
        }
    }

    public AgentState captureState() throws AgentServerException {
        // Capture parameter values
        SymbolValues parameterStates = new SymbolValues("parameters");
        SymbolValues parameterValues = categorySymbolValues.get("parameters");
        if (parameterValues != null && agentDefinition != null) {
            for (Field parameter : agentDefinition.parameters) {
                Symbol symbol = symbolManager.get("parameters", parameter.symbol.name);
                Value parameterValue = parameterValues.get(symbol);
                // TODO: Make sure this is a copy of the value
                Value copy = parameterValue.clone();
                parameterStates.put(symbol, parameterValue.clone());
            }
        }

        // Capture data source values
        // TODO: Rework this
        SymbolValues inputStates = new SymbolValues("inputs");
        SymbolValues inputValues = categorySymbolValues.get("inputs");
        if (agentDefinition.inputs != null)
            for (DataSourceReference dataSource : agentDefinition.inputs) {
                // TODO: What to do?? - Should be map of all captured input values for this data source
                //eventStates.put(event, event.getState().clone());
                Symbol symbol = symbolManager.get("inputs", dataSource.name);
                inputStates.put(symbol, inputValues.get(symbol).clone());
            }
        /*
        if (agentDefinition.inputs != null)
          for (DataSourceReference dataSource: agentDefinition.inputs)
            inputStates.put(dataSource, dataSource.getState());
        */

        // Capture memory values
        SymbolValues memoryStates = new SymbolValues("memory");
        SymbolValues memoryValues = categorySymbolValues.get("memory");
        if (memoryValues != null)
            if (memoryValues != null && agentDefinition.memory != null) {
                for (Field memory : agentDefinition.memory) {
                    Symbol symbol = symbolManager.get("memory", memory.symbol.name);
                    memoryStates.put(symbol, memoryValues.get(symbol).clone());
                }
            }
        log.info("Memory states in capture: " + memoryStates.toString() + " memory values: " + memoryValues
                + " scratchpad values: " + categorySymbolValues.get("scratchpad"));

        // TODO: Capture goal values

        // Capture output values
        SymbolValues outputStates = new SymbolValues("outputs");
        SymbolValues outputValues = categorySymbolValues.get("outputs");
        if (outputValues != null)
            if (outputValues != null && agentDefinition.outputs != null) {
                for (Field output : agentDefinition.outputs) {
                    Symbol symbol = symbolManager.get("outputs", output.symbol.name);
                    outputStates.put(symbol, outputValues.get(symbol).clone());
                }
            }

        // Capture exception history
        List<ExceptionInfo> exceptionStates = new ArrayList<ExceptionInfo>();
        for (ExceptionInfo exceptionInfo : exceptionHistory)
            exceptionStates.add(exceptionInfo.clone());

        // Capture notifications
        ListMap<String, NotificationInstance> notificationStates = notifications.clone();

        // Capture the notification history
        NotificationHistory notificationHistoryStates = notificationHistory.clone();

        // Save the captured state, if it changed
        AgentState newState = new AgentState(System.currentTimeMillis(), symbolManager, parameterStates,
                inputStates, memoryStates, outputStates, exceptionStates, lastDismissedExceptionTime,
                notificationStates, notificationHistoryStates);
        int stateSize = state.size();
        if (stateSize == 0 || !state.get(stateSize - 1).equalValues(newState)) {
            // Limit number of states recorded - roll off the oldest
            if (stateSize >= limitInstanceStatesStored
                    || stateSize >= agentServer.config.getMaximumLimitInstanceStatesStored())
                state.remove(0);

            // Store the new state
            state.add(newState);

            // And persist it
            agentServer.persistence.put(this);
        }

        // Return the captured state
        return newState;
    }

    public void instantiateInputDataSources() throws AgentServerException {
        // Instantiate referenced data sources
        for (DataSourceReference dataSourceReference : agentDefinition.inputs) {
            AgentInstance dataSourceInstance = dataSourceReference.instantiate(this, user,
                    agentDefinition.agentServer);
            dataSourceInstances.put(dataSourceReference, dataSourceInstance);
        }
    }

    public void initializeVariables() throws AgentServerException {
        // Set value for each parameter, as specified or default if not specified
        SymbolValues parameterValues = categorySymbolValues.get("parameters");
        for (Field field : agentDefinition.parameters) {
            // TODO: Should parameters support 'compute' as well?
            // See if user specified an explicit parameter value
            Value valueNode = this.parameterValues.get(field.symbol.name);
            if (valueNode instanceof NullValue) {
                // No explicit value, so use default value from agent definition
                valueNode = field.getDefaultValueNode();
            }
            parameterValues.put(symbolManager.get("parameters", field.symbol.name), valueNode);
        }
        log.info("Initial parameter values for instance " + name + ": "
                + categorySymbolValues.get("parameters").toJson());

        // Capture current output values of all data sources specified as inputs
        captureDataSourceOutputValues();

        // Set default value for each scratchpad field
        SymbolValues scratchpadValues = categorySymbolValues.get("scratchpad");
        for (Field field : agentDefinition.scratchpad) {
            if (field.compute != null) {
                Value newValue = evaluateExpression(field.compute);
                scratchpadValues.put(symbolManager.get("scratchpad", field.symbol.name), newValue);
                log.info("Computed initial scratchpad value for " + field.symbol.name + ": " + newValue.toJson());
            } else
                scratchpadValues.put(symbolManager.get("scratchpad", field.symbol.name),
                        field.getDefaultValueNode());
        }

        // Set default value for each memory field
        log.info("Initialize memory variables for " + name);
        SymbolValues memoryValues = categorySymbolValues.get("memory");
        for (Field field : agentDefinition.memory) {
            if (field.compute != null) {
                Value newValue = evaluateExpression(field.compute);
                memoryValues.put(symbolManager.get("memory", field.symbol.name), newValue);
                log.info("Computed initial memory value for " + field.symbol.name + ": " + newValue.toJson());
            } else
                memoryValues.put(symbolManager.get("memory", field.symbol.name), field.getDefaultValueNode());
        }

        // Set default value for each output field
        SymbolValues outputValues = categorySymbolValues.get("outputs");
        for (Field field : agentDefinition.outputs)
            // TODO: Should this support 'compute' at this stage or is it okay to come later?
            outputValues.put(symbolManager.get("outputs", field.symbol.name), field.getDefaultValueNode());
        log.info(
                "Initial output values for instance " + name + ": " + categorySymbolValues.get("outputs").toJson());

        // Initialize output history
        this.outputHistory.clear();

        // Initialize notifications
        for (NameValue<NotificationDefinition> nameValue : agentDefinition.notifications) {
            NotificationDefinition notificationDefinition = agentDefinition.notifications.get(nameValue.name);
            SymbolValues notificationDetailValues = categorySymbolValues.get("notifications");

            // Initialize default values for detail fields
            MapValue mapValue = null;
            if (notificationDefinition.detail != null) {
                // Construct a map with details values for detail fields
                List<FieldValue> mapFields = new ArrayList<FieldValue>();
                for (Field field : notificationDefinition.detail)
                    mapFields.add(new FieldValue(field.symbol.name, field.getDefaultValueNode()));
                mapValue = new MapValue(ObjectTypeNode.one, (List<Value>) (Object) mapFields);

                // Initialize the notification value with the map of detail fields
                notificationDetailValues.put(symbolManager.get("notifications", notificationDefinition.name),
                        mapValue);
            }

            NotificationInstance notificationInstance = new NotificationInstance(this, notificationDefinition,
                    mapValue);
            notifications.put(nameValue.name, notificationInstance);

            // Compute the timeout value
            notificationInstance.timeout = evaluateExpression(notificationDefinition.timeoutExpression)
                    .getLongValue();
        }

        // Checkpoint the initial output values and trigger dependent agents
        checkpointOutput();
    }

    public void initializeConditionStatus() {
        // Initialize status for conditions
        for (NameValue<AgentCondition> conditionNameValue : agentDefinition.conditions) {
            AgentCondition condition = conditionNameValue.value;
            AgentConditionStatus status = new AgentConditionStatus(condition);
            conditionStatus.put(condition.name, status);
        }
    }

    public void initializeTimerStatus() {
        // Initialize status for timers
        for (NameValue<AgentTimer> timerNameValue : agentDefinition.timers) {
            AgentTimer timer = timerNameValue.value;
            AgentTimerStatus status = new AgentTimerStatus(timer);
            timerStatus.put(timer.name, status);
        }
    }

    public void checkpointOutput() throws AgentServerException {
        // Recompute any computed fields
        SymbolValues outputValues = categorySymbolValues.get("outputs");
        for (Field field : agentDefinition.outputs) {
            if (field.compute != null) {
                Value newValue = evaluateExpression(field.compute);
                outputValues.put(symbolManager.get("outputs", field.symbol.name), newValue);
                log.info("Computed new output value for " + field.symbol.name + ": " + newValue.toJson());
            }
        }
        //log.info("Initial output values for instance " + name + ": " + categorySymbolValues.get("outputs").toJson());

        // Trigger dependent instances if output values of this instance changed
        // TODO: Where else do we need to do this?
        // - Init of instance for initial output values
        SymbolValues currentOutputValues = categorySymbolValues.get("outputs");
        OutputRecord outputRecord = outputHistory.getLatest();
        SymbolValues savedOutputValues = outputRecord == null ? null : outputRecord.output;
        if (savedOutputValues == null || !savedOutputValues.equals(currentOutputValues)) {
            if (savedOutputValues == null)
                log.info("Initial output for " + name + ": " + currentOutputValues);
            else
                log.info("Output changed for " + name + " - #" + outputRecord.sequenceNumber + " old output: "
                        + savedOutputValues + " new output: " + currentOutputValues);

            // Trigger all dependent instances that output has changed
            triggerInputChanged();

            //if (savedOutputValues != null)
            //log.info("equals: " + savedOutputValues.equals(currentOutputValues));

            // Save deep copy of changed output
            outputHistory.add(currentOutputValues.clone());

            // Trigger notifications
            triggerNotifications();
        } else
            log.info("Output unchanged - no triggering");
    }

    public void triggerNotifications() throws AgentServerException {
        // Note: Only called when outputs have changed
        for (String notificationName : notifications) {
            // Get the next notification
            NotificationInstance notificationInstance = notifications.get(notificationName);
            NotificationDefinition notificationDefinition = notificationInstance.definition;

            // Skip "manual" notifications
            if (notificationDefinition.manual)
                continue;

            // Notification may be conditional on some expression
            String condition = notificationDefinition.condition;
            if (condition != null && condition.trim().length() > 0)
                if (!evaluateExpression(condition).getBooleanValue())
                    continue;

            // Queue up the notification
            queueNotify(notificationInstance);
        }
    }

    public long evaluateExpressionLong(String expression) throws AgentServerException {
        return evaluateExpression(expression, false).getLongValue();
    }

    public Value evaluateExpression(String expression) throws AgentServerException {
        return evaluateExpression(expression, false);
    }

    public Value evaluateExpression(String expression, boolean captureInputs) throws AgentServerException {
        try {
            // Compile the script
            // TODO: Cache and reuse compiled scripts
            ScriptParser parser = new ScriptParser(this);
            ExpressionNode expressionNode = parser.parseExpressionString(expression);

            // Optionally capture output field values for data source inputs
            if (captureInputs)
                captureDataSourceOutputValues();

            // Run the compiled expression
            Value valueNode = scriptRuntime.evaluateExpression(expression, expressionNode);

            // Detect expression that exits the instance
            if (deleted) {
                // Yes, remove the deleted instance
                log.info("Expression evaluation of '" + expression + "' is exiting instance '" + name + "'");
                agentServer.removeAgentInstance(this);
                return valueNode;
            }

            // Return the return value of the evaluated expression
            return valueNode;
        } catch (TokenizerException e) {
            throw new AgentServerException(
                    "TokenizerException parsing expression \"" + expression + "\" - " + e.getMessage());
        } catch (ParserException e) {
            throw new AgentServerException(
                    "ParserException parsing expression \"" + expression + "\" - " + e.getMessage());
        }
    }

    public Value runScript(String scriptName) throws TokenizerException, ParserException, SymbolException,
            RuntimeException, JSONException, AgentServerException {
        return runScript(scriptName, true);
    }

    public Value runScript(String scriptName, boolean captureInputs) throws TokenizerException, ParserException,
            SymbolException, RuntimeException, JSONException, AgentServerException {
        return runScript(scriptName, null, captureInputs);
    }

    public Value runScript(String scriptName, List<Value> arguments) throws TokenizerException, ParserException,
            SymbolException, RuntimeException, JSONException, AgentServerException {
        return runScript(scriptName, arguments, true);
    }

    public Value runScript(String scriptName, List<Value> arguments, boolean captureInputs)
            throws TokenizerException, ParserException, SymbolException, RuntimeException, JSONException,
            AgentServerException {
        // Reset script status
        scriptStartTime.put(scriptName, null);
        scriptEndTime.put(scriptName, null);
        scriptReturnValue.put(scriptName, null);

        // TODO: Consider scripts with parameters

        // Make sure script name is defined
        if (!agentDefinition.scripts.containsKey(scriptName)) {
            // TODO: What to do? For now no-op
            scriptStatus.put(scriptName, "undefined");
            return NullValue.one;
            //throw new RuntimeException("Undefined script name, '" + scriptName + "' for agent " + name);
        }

        // Record start time for script
        scriptStartTime.put(scriptName, System.currentTimeMillis());

        // TODO: Record script status: never ran, compile errors, exceptions, aborted, timed-out

        // Compile the script
        // TODO: Cache and reuse compiled scripts
        scriptStatus.put(scriptName, "compiling");
        ScriptParser parser = new ScriptParser(this);
        String script = agentDefinition.scripts.get(scriptName).script;
        // TODO: Do something with script definition
        ScriptNode scriptNode = parser.parseScriptString(script);

        // Optionally capture output field values for data source inputs
        if (captureInputs)
            captureDataSourceOutputValues();

        // Run the compiled script
        scriptStatus.put(scriptName, "running");
        Value valueNode = scriptRuntime.runScript(scriptName, scriptNode, arguments);
        scriptStatus.put(scriptName, "ran");

        // Record the script return value, if any
        scriptReturnValue.put(scriptName, valueNode);

        // Record end time for script
        scriptEndTime.put(scriptName, System.currentTimeMillis());

        // Capture state, if changed
        captureState();

        log.info("Finished running script '" + scriptName + "' for instance '" + name + "'");

        // Detect script that exits the instance
        if (deleted) {
            // Yes, remove the deleted instance
            log.info("Script '" + scriptName + "' is exiting instance '" + name + "'");
            agentServer.removeAgentInstance(this);
            return valueNode;
        }

        // Trigger dependent instances if output values of this instance changed
        checkpointOutput();

        // Return the return value node for the script
        return valueNode;
    }

    public Value runScriptString(String script) throws AgentServerException {
        return runScriptString(script, true);
    }

    public Value runScriptString(String script, boolean captureInputs) throws AgentServerException {
        try {
            // Compile the script
            // TODO: Cache and reuse compiled scripts
            ScriptParser parser = new ScriptParser(this);
            ScriptNode scriptNode = parser.parseScriptString(script);

            // Optionally capture output field values for data source inputs
            if (captureInputs)
                captureDataSourceOutputValues();

            // Run the compiled script
            Value valueNode = scriptRuntime.runScript(script, scriptNode);

            // Capture state
            captureState();

            // Trigger dependent instances if output values of this instance changed
            checkpointOutput();

            // Return the return value node for the script
            return valueNode;
        } catch (TokenizerException e) {
            throw new AgentServerException(
                    "TokenizerException parsing script string \"" + script + "\" - " + e.getMessage());
        } catch (ParserException e) {
            throw new AgentServerException(
                    "ParserException parsing expression \"" + script + "\" - " + e.getMessage());
        }
    }

    public AgentState getCurrentState() {
        int numStates = state.size();
        if (numStates == 0)
            return null;
        else
            return state.get(numStates - 1);
    }

    public JSONObject toJson() throws AgentServerException {
        return toJson(true);
    }

    public JSONObject toJson(boolean includeState) throws AgentServerException {
        return toJson(includeState, -1);
    }

    public JSONObject toJson(boolean includeState, int stateCount) throws AgentServerException {
        try {
            JSONObject agentJson = new JsonListMap();
            agentJson.put("user", user.id);
            agentJson.put("name", name);
            agentJson.put("definition", agentDefinition.name);
            agentJson.put("description", description == null ? "" : description);
            agentJson.put("instantiated", DateUtils.toRfcString(timeInstantiated));
            agentJson.put("updated", timeUpdated > 0 ? DateUtils.toRfcString(timeUpdated) : "");

            agentJson.put("trigger_interval", triggerIntervalExpression);
            agentJson.put("reporting_interval", reportingIntervalExpression);
            agentJson.put("public_output", publicOutput);
            agentJson.put("limit_instance_states_stored", limitInstanceStatesStored);
            agentJson.put("enabled", enabled);

            // Return most recent parameter values
            JSONObject currentParameterValuesJson = new JsonListMap();
            for (Field parameter : agentDefinition.parameters)
                currentParameterValuesJson.put(parameter.symbol.name,
                        getParameter(parameter.symbol.name).getValue());
            agentJson.put("parameter_values", currentParameterValuesJson);

            // Summarize activity of instance
            agentJson.put("inputs_changed", lastInputsChanged > 0 ? DateUtils.toRfcString(lastInputsChanged) : "");
            agentJson.put("triggered", lastTriggered > 0 ? DateUtils.toRfcString(lastTriggered) : "");
            agentJson.put("outputs_changed",
                    outputHistory.size() > 0
                            ? DateUtils.toRfcString(outputHistory.get(outputHistory.size() - 1).time)
                            : "");
            agentJson.put("status", getStatus());

            if (includeState) {
                // Generate array of state history
                JSONArray stateHistoryJson = new JSONArray();

                // Default and limit count of states to return
                int historySize = state.size();
                if (stateCount <= 0)
                    stateCount = agentServer.config.getDefaultLimitInstanceStatesReturned();
                if (stateCount <= 0)
                    stateCount = historySize;
                if (stateCount > historySize)
                    stateCount = historySize;
                int limitCount = agentServer.config.getMaximumLimitInstanceStatesReturned();
                if (stateCount > limitCount)
                    stateCount = limitCount;
                int startIndex = historySize - stateCount;
                for (int i = startIndex; i < historySize; i++)
                    stateHistoryJson.put(state.get(i).toJson());

                agentJson.put("state", stateHistoryJson);
            }

            return agentJson;
        } catch (JSONException e) {
            e.printStackTrace();
            throw new AgentServerException("JSON exception in AgentInstance.toJson - " + e.getMessage());
        }
    }

    public Value getEvent(String fieldName) throws SymbolException {
        return categorySymbolValues.get("events").get(symbolManager.get("events", fieldName));
    }

    public Value getInput(String fieldName) throws SymbolException {
        return categorySymbolValues.get("inputs").get(symbolManager.get("inputs", fieldName));
    }

    public Value getMemory(String fieldName) throws SymbolException {
        return categorySymbolValues.get("memory").get(symbolManager.get("memory", fieldName));
    }

    public Value getOutput(String fieldName) throws SymbolException {
        return categorySymbolValues.get("outputs").get(symbolManager.get("outputs", fieldName));
    }

    public Value getParameter(String fieldName) throws SymbolException {
        return categorySymbolValues.get("parameters").get(symbolManager.get("parameters", fieldName));
    }

    public void putInput(String fieldName, Value value) throws SymbolException {
        categorySymbolValues.get("inputs").put(symbolManager.get("inputs", fieldName), value);
    }

    public void putMemory(String fieldName, Value value) throws SymbolException {
        categorySymbolValues.get("memory").put(symbolManager.get("memory", fieldName), value);
    }

    public void addReference(AgentInstance agentInstance) {
        dependentInstances.add(agentInstance);
    }

    public void removeReference(AgentInstance agentInstance) {
        dependentInstances.remove(agentInstance);
    }

    public void release() throws AgentServerException {
        if (dependentInstances.size() > 0)
            // TODO: Should we maybe mark for auto-release when dependents do go away
            throw new AgentServerException("Can't release an instance that has dependents");

        // Remove references for all data sources
        for (DataSourceReference dataSourceReference : dataSourceInstances.keySet())
            dataSourceInstances.get(dataSourceReference).removeReference(this);
        dataSourceInstances.clear();
    }

    public void triggerInputChanged() throws AgentServerException {
        // Trigger each instance that is dependent on this instance as an input
        for (AgentInstance agentInstance : dependentInstances)
            triggerInputChanged(agentInstance);
    }

    public void triggerInputChanged(AgentInstance dataSourceInstance) throws AgentServerException {
        // Create a new trigger activity for data source change
        AgentActivityTriggerInputChanged triggerActivity = new AgentActivityTriggerInputChanged(dataSourceInstance,
                this);

        // Schedule the trigger activity
        if (AgentScheduler.singleton != null)
            AgentScheduler.singleton.add(triggerActivity);
    }

    public void update(AgentServer agentServer, AgentInstance updated)
            throws SymbolException, JSONException, AgentServerException {
        // TODO: Only update time if there are any actual changes
        this.timeUpdated = System.currentTimeMillis();

        if (updated.description != null)
            this.description = updated.description;

        if (updated.parameterValues != null)
            this.parameterValues = updated.parameterValues;
        // TODO: Do we need to update Symbol Manager?

        if (updated.enabled != null)
            this.enabled = updated.enabled;

        if (updated.triggerIntervalExpression != null)
            this.triggerIntervalExpression = updated.triggerIntervalExpression;

        if (updated.reportingIntervalExpression != null)
            this.reportingIntervalExpression = updated.reportingIntervalExpression;

        // Persist the changes
        agentServer.persistence.put(this);
    }

    static public AgentInstance fromJson(AgentServer agentServer, String agentJsonSource)
            throws AgentServerException, SymbolException, JSONException, ParseException, TokenizerException,
            ParserException {
        return fromJson(agentServer, null, new JSONObject(agentJsonSource), null, false);
    }

    static public AgentInstance fromJson(AgentServer agentServer, JSONObject agentJson) throws AgentServerException,
            SymbolException, JSONException, ParseException, TokenizerException, ParserException {
        return fromJson(agentServer, null, agentJson, null, false);
    }

    static public AgentInstance fromJson(AgentServer agentServer, User user, JSONObject agentJson,
            AgentDefinition agentDefinition, boolean update) throws AgentServerException, SymbolException,
            JSONException, ParseException, TokenizerException, ParserException {
        // Parse the JSON for the agent instance

        // If we have the user, ignore user from JSON
        if (user == null) {
            String userId = agentJson.optString("user");
            if (userId == null || userId.trim().length() == 0)
                throw new AgentServerException("Agent instance user id ('user') is missing");
            user = agentServer.getUser(userId);
            if (user == User.noUser)
                throw new AgentServerException("Agent instance user id does not exist: '" + userId + "'");
        }

        // Parse the agent instance name
        String agentInstanceName = agentJson.optString("name");
        if (!update && (agentInstanceName == null || agentInstanceName.trim().length() == 0))
            throw new AgentServerException("Agent instance name ('name') is missing");

        // Parse the agent definition name - but ignore for update since it can't be changed
        if (!update) {
            String agentDefinitionName = agentJson.optString("definition");
            if (agentDefinitionName == null || agentDefinitionName.trim().length() == 0)
                throw new AgentServerException(
                        "Agent instance definition name ('definition') is missing for user '" + user.id + "'");

            // Check if referenced agent definition exists
            agentDefinition = agentServer.getAgentDefinition(user, agentDefinitionName);
            if (agentDefinition == null)
                throw new AgentServerException(
                        "Agent instance '" + agentInstanceName + "' references agent definition '"
                                + agentDefinitionName + "' which does not exist for user '" + user.id + "'");
        }

        // Parse the agent instance description
        String agentDescription = agentJson.optString("description", null);
        if (!update && (agentDescription == null || agentDescription.trim().length() == 0))
            agentDescription = "";

        // Parse the agent instance parameter values
        String invalidParameterNames = "";
        SymbolManager symbolManager = new SymbolManager();
        SymbolTable symbolTable = symbolManager.getSymbolTable("parameter_values");
        JSONObject parameterValuesJson = null;
        SymbolValues parameterValues = null;
        if (agentJson.has("parameter_values")) {
            // Parse the parameter values
            parameterValuesJson = agentJson.optJSONObject("parameter_values");
            parameterValues = SymbolValues.fromJson(symbolTable, parameterValuesJson);

            // Validate that they are all valid agent definition parameters
            Map<String, Value> treeMap = new TreeMap<String, Value>();
            for (Symbol symbol : parameterValues)
                treeMap.put(symbol.name, null);
            for (String parameterName : treeMap.keySet())
                if (!agentDefinition.parameters.containsKey(parameterName))
                    invalidParameterNames += (invalidParameterNames.length() > 0 ? ", " : "") + parameterName;
            if (invalidParameterNames.length() > 0)
                throw new AgentServerException("Parameter names for agent instance " + agentInstanceName
                        + " are not defined for referenced agent definition " + agentDefinition.name + ": "
                        + invalidParameterNames);
        }

        String triggerInterval = JsonUtils.getString(agentJson, "trigger_interval",
                update ? null : AgentDefinition.DEFAULT_TRIGGER_INTERVAL_EXPRESSION);
        String reportingInterval = JsonUtils.getString(agentJson, "reporting_interval",
                update ? null : AgentDefinition.DEFAULT_REPORTING_INTERVAL_EXPRESSION);

        Boolean publicOutput = null;
        if (agentJson.has("public_output"))
            publicOutput = agentJson.optBoolean("public_output");
        else if (update)
            publicOutput = null;
        else
            publicOutput = false;

        int limitInstanceStatesStored = agentJson.optInt("limit_instance_states_stored", -1);

        Boolean enabled = null;
        if (agentJson.has("enabled"))
            enabled = agentJson.optBoolean("enabled");
        else if (update)
            enabled = null;
        else
            enabled = true;

        // Parse creation and modification timestamps
        String created = agentJson.optString("instantiated", null);
        long timeInstantiated = -1;
        try {
            timeInstantiated = created != null ? DateUtils.parseRfcString(created) : -1;
        } catch (ParseException e) {
            throw new AgentServerException("Unable to parse created date ('" + created + "') - " + e.getMessage());
        }
        String modified = agentJson.optString("updated", null);
        long timeUpdated = -1;
        try {
            timeUpdated = modified != null ? (modified.length() > 0 ? DateUtils.parseRfcString(modified) : 0) : -1;
        } catch (ParseException e) {
            throw new AgentServerException("Unable to parse updated date ('" + modified + "') - " + e.getMessage());
        }

        // Parse state history
        List<AgentState> state = null;
        if (agentJson.has("state")) {
            JSONArray stateHistoryJson = agentJson.optJSONArray("state");
            int numStates = stateHistoryJson.length();
            state = new ArrayList<AgentState>();
            for (int i = 0; i < numStates; i++) {
                JSONObject stateJson = stateHistoryJson.optJSONObject(i);
                AgentState newState = AgentState.fromJson(stateJson, symbolManager);
                state.add(newState);

            }
        }

        // Validate keys
        JsonUtils.validateKeys(agentJson, "Agent instance",
                new ArrayList<String>(Arrays.asList("user", "name", "definition", "description", "parameter_values",
                        "trigger_interval", "reporting_interval", "enabled", "instantiated", "updated", "state",
                        "status", "inputs_changed", "triggered", "outputs_changed", "public_output",
                        "limit_instance_states_stored")));

        AgentInstance agentInstance = new AgentInstance(user, agentDefinition, agentInstanceName, agentDescription,
                parameterValues, triggerInterval, reportingInterval, publicOutput, limitInstanceStatesStored,
                enabled, timeInstantiated, timeUpdated, state, update, false);

        // Return the new agent instance
        return agentInstance;
    }

    public long getReportingInterval() throws AgentServerException {
        // Get the desired reporting interval
        long reportingInterval = evaluateExpressionLong(reportingIntervalExpression);

        // May need to throttle it down
        long minimumReportingInterval = agentServer.getMinimumReportingInterval();
        if (reportingInterval < minimumReportingInterval) {
            log.info("Throttling reporting_interval " + reportingInterval + " for " + name + " down to minimum of "
                    + minimumReportingInterval);
            reportingInterval = minimumReportingInterval;
        }
        return reportingInterval;
    }

    public long getTriggerInterval() throws AgentServerException {
        // Get the desired trigger interval
        long triggerInterval = evaluateExpressionLong(triggerIntervalExpression);

        // May need to throttle it down
        long minimumTriggerInterval = agentServer.getMinimumTriggerInterval();
        if (triggerInterval < minimumTriggerInterval) {
            log.info("Throttling trigger_interval " + triggerInterval + " for " + name + " down to minimum of "
                    + minimumTriggerInterval);
            triggerInterval = minimumTriggerInterval;
        }
        return triggerInterval;
    }

    public long getTriggerTime() throws AgentServerException {
        // If we have never triggered, we can accept input immediately
        if (lastTriggered == 0)
            return System.currentTimeMillis();
        else
            // Otherwise we can't take input until our trigger interval expires
            // Note: That may be a time in the past, but that is okay and means immediately
            return lastTriggered + getTriggerInterval();
    }

    public void setState(List<AgentState> state, boolean update) throws AgentServerException {
        int stateSize = state == null ? 0 : state.size();
        if (stateSize > 0) {
            // Restore saved state history
            this.state = state;

            // Now initialize all variables as per saved state
            AgentState currentState = state.get(stateSize - 1);

            // Restore parameter values
            SymbolValues parameterValues = categorySymbolValues.get("parameters");
            for (Symbol symbol : currentState.parameterValues)
                parameterValues.put(symbolManager.get("parameters", symbol.name),
                        currentState.parameterValues.get(symbol.name).clone());

            // Restore captured inputs
            SymbolValues inputValues = categorySymbolValues.get("inputs");
            for (Symbol symbol : currentState.inputValues)
                inputValues.put(symbolManager.get("inputs", symbol.name),
                        currentState.inputValues.get(symbol.name).clone());

            // Instantiate data sources used as inputs
            if (!update)
                instantiateInputDataSources();

            // Restore memory
            SymbolValues memoryValues = categorySymbolValues.get("memory");
            for (Symbol symbol : currentState.memoryValues)
                memoryValues.put(symbolManager.get("memory", symbol.name),
                        currentState.memoryValues.get(symbol.name).clone());

            // Restore outputs
            SymbolValues outputValues = categorySymbolValues.get("outputs");
            for (Symbol symbol : currentState.outputValues)
                outputValues.put(symbolManager.get("outputs", symbol.name),
                        currentState.outputValues.get(symbol.name).clone());

            // Restore output history
            outputHistory = new OutputHistory();
            SymbolValues prevOutputValues = null;
            for (AgentState agentState : this.state) {
                SymbolValues outputValues2 = agentState.outputValues;
                if (prevOutputValues == null || !outputValues2.equals(prevOutputValues))
                    outputHistory.add(outputValues2, agentState.time);
                prevOutputValues = outputValues2;
            }

            // Restore exception history
            exceptionHistory = new ArrayList<ExceptionInfo>();
            for (ExceptionInfo exceptionInfo : currentState.exceptionHistory)
                exceptionHistory.add(exceptionInfo.clone());
            lastDismissedExceptionTime = currentState.lastDismissedExceptionTime;

            // Restore notification history
            notificationHistory = new NotificationHistory();
            for (NotificationRecord notificationRecord : currentState.notificationHistory) {
                notificationRecord.notificationInstance.agentInstance = this;
                notificationHistory.add(notificationRecord);
            }
        } else {
            // Simply initialize all variables to default values
            this.state = new ArrayList<AgentState>();

            // Instantiate data sources used as inputs
            if (!update)
                instantiateInputDataSources();

            // TODO: Somewhere, we need detection of loops in input dependencies

            // Initialize parameters, capture inputs, and set memory and outputs to default values
            if (!update && state == null)
                // TODO: Maybe pass incoming state here to be incorporated with default behavior
                initializeVariables();
        }
    }

    public AgentInstance getDataSourceInstance(String dataSourceName) {
        for (DataSourceReference dsr : dataSourceInstances.keySet())
            if (dsr.name.equals(dataSourceName))
                return dataSourceInstances.get(dsr);
        return null;
    }

    public String getDataSourceInstanceName(String dataSourceName) {
        for (DataSourceReference dsr : dataSourceInstances.keySet())
            if (dsr.name.equals(dataSourceName))
                return dataSourceInstances.get(dsr).name;
        return null;
    }

    public NotificationInstance getPendingNotification() {
        for (String name : notifications) {
            NotificationInstance notification = notifications.get(name);
            if (notification.pending)
                return notification;
        }
        return null;
    }

    public String getStatus() {
        NotificationInstance pendingNotification = getPendingNotification();
        if (exceptionHistory.size() > 0
                && exceptionHistory.get(exceptionHistory.size() - 1).time > lastDismissedExceptionTime)
            return "exception: " + exceptionHistory.get(exceptionHistory.size() - 1).message;
        else if (!ranInit)
            return "starting";
        else if (pendingNotification != null) {
            if (pendingSuspended)
                return "notification_pending_suspended: " + pendingNotification.definition.name;
            else
                return "notification_pending_active: " + pendingNotification.definition.name;
        } else if (enabled)
            return "active";
        else
            return "disabled";
    }

    public void queueNotify(String notificationName) throws AgentServerException {
        NotificationInstance notificationInstance = notifications.get(notificationName);
        if (notificationInstance == null)
            throw new AgentServerException("Undefined notification name: " + notificationName);

        queueNotify(notificationInstance);
    }

    public void queueNotify(NotificationInstance notificationInstance) throws AgentServerException {
        // Create a new activity for the notification
        AgentActivityNotification agentActivityNotification = new AgentActivityNotification(this, 0,
                notificationInstance);

        // Queue up the new activity
        // TODO: This needs to synchronized
        AgentScheduler.singleton.add(agentActivityNotification);

    }

    public void notify(NotificationInstance notificationInstance) throws AgentServerException {
        // Store info for the notification
        notificationInstance.pending = !notificationInstance.definition.type.equals("notify_only");
        notificationInstance.timeNotified = System.currentTimeMillis();
        notificationInstance.timeResponse = 0;
        notificationInstance.response = "no_response";
        notificationInstance.responseChoice = "no_choice";
        notificationInstance.comment = "";

        // May need to suspend instance for this notification
        if (notificationInstance.definition.suspend)
            pendingSuspended = true;

        //Save notification state history
        notificationHistory.add(notificationInstance);

        // Perform the notification - email-only, for now
        if (user.email != null && user.email.trim().length() > 0) {
            if (suppressEmail)
                log.warn("Email notification suppressed by suppressEmail flag for instance " + name);
            else if (!agentServer.config.getMailAccessEnabled())
                log.warn("Email notification suppressed by mail_access_enabled = false for instance " + name);
            else {
                MailNotification mailNotification = new MailNotification(agentServer);
                mailNotification.notify(notificationInstance);
            }
        } else
            log.info("No email notification since user '" + user.id + "' has no email address");

        // And capture full agent state and persist it
        captureState();
    }

    public void respondToNotification(NotificationInstance notificationInstance, String response,
            String responseChoice, String comment) throws AgentServerException {
        // Validate the response
        if (!NotificationInstance.responses.contains(response))
            throw new AgentServerException("Invalid response for notification '"
                    + notificationInstance.definition.name + "' of agent instance '" + name + "': " + response);

        // Store the response and choice
        notificationInstance.response = response;
        if (responseChoice != null)
            notificationInstance.responseChoice = responseChoice;
        if (comment != null)
            notificationInstance.comment = comment;

        // Clear pending status
        notificationInstance.pending = false;
        pendingSuspended = false;

        //Save notification state history
        notificationHistory.add(notificationInstance);

        // And capture full agent state and persist it
        captureState();

        // Now run an optional script based on the response
        ScriptDefinition scriptDefinition = notificationInstance.definition.scripts.get(response);
        if (scriptDefinition != null) {
            runScriptString(scriptDefinition.script);
            // TODO: Pass script name/description to runScriptString
        } else
            log.info("No script named '" + response + "' to run in response to notification '"
                    + notificationInstance.definition.name + "' for agent instance '" + name + "'");
    }

    public void deReference(AgentInstance agentInstance) throws AgentServerException {
        // Remove an agent instance from the dependents list of this agent instance
        dependentInstances.remove(agentInstance);

        // If no more dependents, auto-delete this instance if it was auto-created
        if (dependentInstances.size() == 0 && autoCreated) {
            log.info("Auto-deleting agent instance " + name + " since all dependents have gone away");
            agentServer.removeAgentInstance(this);
        } else {
            log.info("Agent instance " + name + " still has " + dependentInstances.size()
                    + " dependents after dependent " + agentInstance.name + " de-references it");
        }
    }

    public void deReferenceInputs() throws AgentServerException {
        // De-reference agents associated with each input
        for (DataSourceReference dsr : dataSourceInstances.keySet()) {
            AgentInstance agentInstance = dataSourceInstances.get(dsr);
            agentInstance.deReference(this);
        }
    }

    public void parseScripts() throws AgentServerException {
        parsedScripts.clear();
        if (agentDefinition != null && agentDefinition.scripts != null)
            for (NameValue<ScriptDefinition> scriptDefinitionNameValue : agentDefinition.scripts) {
                ScriptDefinition scriptDefinition = scriptDefinitionNameValue.value;
                ScriptParser parser = new ScriptParser(this);
                String script = agentDefinition.scripts.get(scriptDefinition.name).script;
                try {
                    ScriptNode scriptNode = parser.parseScriptString(script);
                    parsedScripts.add(scriptNode);
                } catch (TokenizerException e) {
                    throw new AgentServerException("TokenizerException: " + e.getMessage());
                } catch (ParserException e) {
                    throw new AgentServerException("TokenizerException: " + e.getMessage());
                }
            }
    }

    public ScriptNode get(String functionName, List<TypeNode> argumentTypes) {
        return parsedScripts.get(functionName, argumentTypes);
    }
}