Dynamic goals

Goals in glove-memory describe what the agent is working toward. Each program contains ordered goals with stable keys, objectives, and checklist items. As the agent learns more, it can revise the program without losing completed work or deferred follow-ups.

A returning client might need a short update instead of a full intake. Keep the verified identity, retire irrelevant questions, and add a goal for the new development. The application chooses its practice rules; Glove handles persistence, progress, revisions, and tool integration.

Attach the runner

goals.tstypescript
import { defineGoalProgram, InMemoryGoalAdapter } from "glove-memory";
import { useGoalRunner } from "glove-memory/tools";

const { runner, refresh } = useGoalRunner(glove, new InMemoryGoalAdapter(), {
  scope: { subject: "firm:1/matter:2", key: "intake", agent: "assistant" },
  actor: "intake-agent",
  source: "conversation:3",
  tools: { deny: ["start"] }, // The host chooses the initial program.
});

await runner.start(defineGoalProgram({
  key: "client-intake",
  goals: [{
    key: "identity",
    title: "Confirm identity",
    objective: "Know who is speaking",
    items: [{ key: "client", label: "Verify the client", locked: true }],
  }],
}));

await runner.update({
  goalKey: "identity", completed: ["client"],
  reason: "Returning client verified from stored records",
});

The tools and direct runner share the same state operations. Scope is the exact tuple (subject, key, agent?); qualify subjects with the tenant and matter or conversation identity. The model cannot select another scope. Use an application-owned GoalAdapterfor durable storage; the in-memory adapter lasts one process.

Revise when context changes

typescript
const current = await runner.inspect();
if (!current) throw new Error("Start a goal program first");

await runner.revise({
  ...current.program,
  goals: [...current.program.goals, {
    key: "updates",
    title: "Matter updates",
    objective: "Collect changes since the previous conversation",
    items: [
      { key: "changes", label: "New developments" },
      { key: "documents", label: "New documents" },
    ],
  }],
}, { ifVersion: current.version, reason: "This is an existing-matter follow-up" });

await runner.update({
  goalKey: "updates", completed: ["changes"], deferred: ["documents"],
  reason: "Update recorded; client will send documents tomorrow",
});

Revisions supply the full ordered program. Stable keys preserve progress, including when a removed item is reintroduced. Use new keys for new obligations. Remove obsolete definitions or mark them retired: true; their history and progress remain. A new pending item reopens a completed goal. Program identity cannot change within a scope: use another set for a different program.

Set locked: true on a goal or item to prevent editing, removing, retiring, or unlocking its definition. Locks do not constrain dispositions. Hosts can enforce stricter rules through the side-effect-free validateChange callback, which runs before every commit attempt and rejects by throwing.

Track progress without losing follow-ups

Untouched items are pending. completed means done; deferred and declined settle progression while retaining done: false. The first goal with unresolved live items is active. Updates can target any goal, including a later goal whose answers are already known.

Deferred work stays visible after the program completes, including deferrals belonging to removed definitions. Complete or decline it later; reopened makes a live item pending again. Restore retired definitions before reopening them. Repeated dispositions are no-ops; unknown or repeated keys reject the whole update.

Agent tools

ToolPurpose
glove_goal_statusRead definitions, progress, version, and deferred work.
glove_goal_startStart a program without resetting an existing set.
glove_goal_updateSet item dispositions with a reason and expected version.
glove_goal_reviseRevise the definition set with a reason and expected version.
glove_goal_historyInspect saved snapshots, reasons, and provenance.

Tool allow/deny selection narrows model access while the host retains the full runner. For custom text or voice surfaces, use GoalRunner and buildGoalRunnerTools directly.

Configure the agent as goals progress

Host-defined onEnter, onComplete, and onReopen hooks receive the goal, historical status, transition, reason, and a stable idempotency key. Mounted hooks also receive the typed glove runnable, so they can fold tools or switch its model during a turn. Hook code stays in the host; editable goal definitions cannot contain executable callbacks.

typescript
useGoalRunner(glove, adapter, {
  scope,
  hooks: {
    onEnter({ glove, goal }) {
      if (goal.definition.key === "evidence") glove.setModel(evidenceModel);
    },
  },
  configure({ glove, status }) {
    // Reapply current state to a fresh runnable after a restart.
    glove.setModel(status?.activeGoal === "evidence" ? evidenceModel : intakeModel);
  },
});

Transitions are saved atomically with progress. Separate dispatch receipts use leases and owner-fenced acknowledgements. Completed effects are skipped on restart; failed effects can resume with runner.resumeHooks() using the same idempotency key. Delivery is at least once: external effects must deduplicate that key. Configure an appropriate hookLeaseMs for long effects.

configure reapplies current state after writes and before requests, including with prompt injection disabled. It must be idempotent and must not mutate goal state or call refresh. Async calls are serialized. To select constructor options or remove tools, read saved status before building a new runnable for the next request. The existing fold method adds tools; check for duplicates.

Entry means a goal became active; completion means all live items were settled, including deferrals and declines; reopening means a completed goal has unresolved work. Retirement is not completion. Hooks receive the historical transition state, while configure receives current state.

Storage and conflicts

A GoalAdapter implements get(scope) and commit(scope, next, { ifVersion }). Writes must atomically persist definitions, progress, and append-only history with compare-and-set semantics; null means create-if-absent. Return detached snapshots and throw GoalConflictError on a mismatch. The runner assigns versions, timestamps, and provenance.

Definition revisions and model progress updates require an explicit version. On conflict, read status and reconsider. Direct host updates can omit the version to retry disjoint item changes; same-item races and concurrent definition changes still surface conflicts. History stores complete snapshots, so restoring a runner needs no definition registry. Plan durable storage capacity for that growing history.

Optional onChange runs after commit. If it fails, GoalPostCommitError includes the committed status; the state has not rolled back.

Lifecycle-enabled adapters also implement claimTransition, settleTransition, and getTransitionDispatches. These receipts must survive aggregate commits. A busy lease blocks later effects and a mounted request waits to be retried before running the model. All workers sharing a scope use the same hooks.

Compose with forms and context

Goals, forms, and context append independent transient user-role snapshots at the model-input tail before each iteration, including after tools. The system prompt and saved conversation stay unchanged. External changes appear on the next iteration. refresh() explicitly runs preparation, transition recovery, and host configuration. Requires glove-core 4.0 or newer; runnable proxies forwardaddContextProvider. Use injectStatus: false and renderGoalStatusto supply a custom renderer.

Form-to-goal mappings belong to the application: after validating a form answer, call runner.update for the corresponding checklist item. A scope thunk can select a conversation between requests; keep it stable during an operation. Use separate runnables for concurrent conversations.

Continue with Forms or the other memory subsystems.