agent-lattice 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -73,6 +73,19 @@ The SDK produces the next event only after the loop takes the current one.
73
73
  Slow work in the loop body delays later events without dropping or reordering
74
74
  them, so keep expensive handling off the loop itself.
75
75
 
76
+ `query()` yields these SDK messages:
77
+
78
+ | Type | When | What it carries |
79
+ | --- | --- | --- |
80
+ | `system` | Query start | Session init metadata (model, tools, `session_id`). |
81
+ | `stream_event` | While the model is responding | Raw provider stream event for incremental rendering. |
82
+ | `assistant` | After each model turn is assembled | The `AssistantModelMessage` with text / `tool_use` blocks and provider metadata. |
83
+ | `user` | After a whole tool batch finishes | Tool results as `ToolResultBlock[]`; the prompt is never echoed. |
84
+ | `result` | Once, at the end of the query | Final text, `subtype` (`"success"`, `"interrupted"`, or an error variant), and token usage. |
85
+
86
+ For the exact per-event guarantees see
87
+ [Streaming Events](https://docs.claude-code-sdk.com/concepts/streaming-events/).
88
+
76
89
  Pass `{ stream: false }` to disable model streaming for a query:
77
90
 
78
91
  ```ts
@@ -334,6 +347,29 @@ const result = await agent.prompt("What is 2+2?");
334
347
  console.log(result.result);
335
348
  ```
336
349
 
350
+ ## End The Run From A Tool
351
+
352
+ *Requires 0.16.0 or later.*
353
+
354
+ A tool that has the final answer can end the run itself by returning
355
+ `endTurn: true`. The SDK finishes with `subtype: "success"` and uses that
356
+ tool's text content as the result, without calling the model again:
357
+
358
+ ```ts
359
+ const finish = tool(
360
+ "finish",
361
+ "Submit the final answer and end the run",
362
+ z.object({ answer: z.string() }),
363
+ async ({ answer }) => ({ content: answer, endTurn: true }),
364
+ );
365
+ ```
366
+
367
+ `endTurn` does not cancel the other tools of the same batch — they already
368
+ started concurrently, their `tool_result` blocks still enter the history, and
369
+ `onToolResult` hooks and trace events run for them as usual. Only the next
370
+ model call is skipped. When several tools in a batch set `endTurn`, the first
371
+ one's content becomes the result text.
372
+
337
373
  ## Concurrent Tool Calls
338
374
 
339
375
  The model requests concurrency by returning multiple `tool_use` blocks in one
@@ -694,12 +730,55 @@ const mcp = await connectMCPStreamableHTTPServer("https://mcp.example.com/mcp",
694
730
  type AgentLike<TContext = unknown> = {
695
731
  query(prompt, options?): AsyncGenerator<SDKMessage | TeamRunnerMessage>;
696
732
  prompt(prompt, options?): Promise<SDKResultMessage>;
733
+ interrupt(): void;
697
734
  };
698
735
  ```
699
736
 
737
+ `interrupt()` ends the in-flight model request with an `"interrupted"` result
738
+ (see [Interrupting A Query](#interrupting-a-query)); on a `Team` or
739
+ `TeamRunner` it delegates to the lead/root agent. *Requires 0.16.0 or later.*
740
+
700
741
  That means a team can be used anywhere a callable agent is expected. From the
701
742
  outside, a team is an agent; inside, it can contain a whole organization.
702
743
 
744
+ ## Agent Specs (Templates) And Sessions
745
+
746
+ *Requires 0.15.0 or later.*
747
+
748
+ `createAgent()` returns a live session: one conversation, one history, one
749
+ workspace. `defineAgent()` returns an `AgentSpec` — a template carrying the
750
+ same options but no state. `spawn()` creates an independent session from it:
751
+
752
+ ```ts
753
+ import { agentTool, defineAgent } from "agent-lattice";
754
+
755
+ const reviewerSpec = defineAgent({
756
+ name: "reviewer",
757
+ model: "claude-sonnet-4-5",
758
+ systemPrompt: "You are a senior code reviewer...",
759
+ });
760
+
761
+ // Register the spec: every tool call spawns a fresh session with no memory
762
+ // of previous calls. This is the safe default for reuse.
763
+ const lead = createAgent({
764
+ model: "claude-sonnet-4-5",
765
+ tools: [
766
+ agentTool("review", reviewerSpec, {
767
+ description: "Ask the reviewer to audit a change.",
768
+ }),
769
+ ],
770
+ });
771
+
772
+ // Register a spawned session instead when the target should remember earlier
773
+ // tasks across calls — continuity is an explicit opt-in.
774
+ const reviewSession = reviewerSpec.spawn();
775
+ ```
776
+
777
+ The same union applies to `delegateTool()`. The generated tool description
778
+ states which semantics a target has, so the calling agent knows whether each
779
+ task must be self-contained. Existing code that passes an `AgentLike` keeps
780
+ its current behavior: a long-lived session with history.
781
+
703
782
  ## Team Mailbox Collaboration
704
783
 
705
784
  Use `createTeam()` when you want to talk to one `AgentLike` while it coordinates
@@ -1036,8 +1115,8 @@ console.log(result.result);
1036
1115
  ```
1037
1116
 
1038
1117
  The SDK stores conversation state in memory for the lifetime of the `Agent`
1039
- instance. Persistent transcripts and resume support are intentionally out of
1040
- scope for the first release.
1118
+ instance. To persist it or resume a conversation in another process, attach a
1119
+ `HistoryStore` see [Persistent History And Resume](#persistent-history-and-resume).
1041
1120
 
1042
1121
  An `Agent` is a conversation, not a reusable client. Because the history is
1043
1122
  instance state, starting a query while another is still running would interleave
@@ -1046,6 +1125,54 @@ Create one Agent per concurrent conversation — in a server, per request or per
1046
1125
  user session rather than a shared module-level instance. Sequential reuse, as
1047
1126
  above, is the intended pattern.
1048
1127
 
1128
+ ## Persistent History And Resume
1129
+
1130
+ *Requires 0.16.0 or later.*
1131
+
1132
+ Pass a `HistoryStore` to seed an Agent's history from durable storage and have
1133
+ every later write mirrored back. `createJsonlHistoryStore()` persists one JSON
1134
+ message per line:
1135
+
1136
+ ```ts
1137
+ import { createAgent, createJsonlHistoryStore } from "agent-lattice";
1138
+
1139
+ const historyStore = createJsonlHistoryStore({
1140
+ path: ".agent-sessions/ada.jsonl",
1141
+ });
1142
+
1143
+ const agent = createAgent({
1144
+ apiKey: process.env.DEEPSEEK_API_KEY,
1145
+ baseURL: "https://api.deepseek.com/anthropic",
1146
+ model: "deepseek-v4-flash",
1147
+ historyStore,
1148
+ });
1149
+
1150
+ // The first query lazily loads any history the store already holds, then the
1151
+ // new prompt continues from it. Every user, assistant, and tool_result message
1152
+ // is appended to the file as it lands.
1153
+ const result = await agent.prompt("What is my name?");
1154
+
1155
+ // A copy of the live history, safe to inspect or mutate.
1156
+ const transcript = await agent.getHistory();
1157
+ ```
1158
+
1159
+ The store contract is three methods — `load()`, `append(message)`, and
1160
+ `replace(messages)` — each synchronous or returning a promise:
1161
+
1162
+ - `load()` runs once per Agent lifetime, lazily before the first query (the
1163
+ constructor cannot be async). Resuming across processes is simply a new
1164
+ Agent over the same store; the "one Agent, one conversation" rule is
1165
+ unchanged.
1166
+ - `append(message)` follows every message added to the history.
1167
+ - `replace(messages)` follows compaction, which rewrites the whole history —
1168
+ a store must support full replacement, not just appends.
1169
+
1170
+ The JSONL store's `load()` skips malformed lines rather than failing, so a
1171
+ torn final write does not lose the rest of the transcript. By default a
1172
+ failing store is swallowed and the conversation continues in memory only,
1173
+ mirroring the tracer's failure semantics; set `failOnError: true` on the store
1174
+ to propagate store errors out of `query()` instead.
1175
+
1049
1176
  ## Deadlines
1050
1177
 
1051
1178
  `QueryOptions.signal` bounds a whole query — every model request, tool call, and
@@ -1075,6 +1202,32 @@ Both limits are enforced by the SDK rather than delegated. `ModelRequest` carrie
1075
1202
  also races the call, so a `ModelClient` that honours neither cannot stall the
1076
1203
  loop indefinitely. Losing that race abandons the call rather than cancelling it.
1077
1204
 
1205
+ ## Interrupting A Query
1206
+
1207
+ *Requires 0.16.0 or later.*
1208
+
1209
+ `agent.interrupt()` ends the current query without tearing the conversation
1210
+ down. Where `QueryOptions.signal` terminates the query with `"error_abort"`,
1211
+ `interrupt()` aborts only the in-flight model request and finishes with
1212
+ `subtype: "interrupted"` — normal control flow, so `is_error` stays `false`.
1213
+ As on abort, the partial assistant message is dropped, but every completed
1214
+ turn stays in the history, so the host can continue the same Agent with a new
1215
+ `query()` that injects its own message:
1216
+
1217
+ ```ts
1218
+ const pending = agent.prompt("Draft the release notes.");
1219
+ agent.interrupt(); // e.g. the user typed a correction
1220
+ const result = await pending;
1221
+ // result.subtype === "interrupted"
1222
+
1223
+ await agent.prompt("Actually, skip 0.15.x and cover 0.16.0 only.");
1224
+ ```
1225
+
1226
+ An interrupt that lands while a tool batch is executing takes effect once the
1227
+ batch completes: its tool results are written to history first, and the query
1228
+ ends `"interrupted"` before the next model call. `interrupt()` is a no-op when
1229
+ no query is running.
1230
+
1078
1231
  ## Token Usage And Truncation
1079
1232
 
1080
1233
  Every `result` message reports `usage`, summed over the model requests in that
@@ -1097,3 +1250,13 @@ to distinguish a complete answer from a truncated one.
1097
1250
  Usage comes from the model client. The built-in Anthropic client fills it in from
1098
1251
  the response, including the streaming path; a custom `ModelClient` that omits
1099
1252
  `usage` produces zeroed counts rather than an error.
1253
+
1254
+ Assistant messages also carry provider response metadata: `providerResponseId`
1255
+ is the provider-assigned response id and `model` is the model that actually
1256
+ served the response, which may differ from the requested `AgentOptions.model`.
1257
+ The built-in Anthropic client fills both in on streaming and non-streaming
1258
+ requests; a custom `ModelClient` may set them on the `AssistantModelMessage` it
1259
+ returns. Both fields are optional and absent when the client does not report
1260
+ them.
1261
+
1262
+ *Requires 0.16.0 or later.*
package/dist/index.d.ts CHANGED
@@ -34,6 +34,8 @@ export type AgentLikeEvent = SDKMessage | TeamRunnerMessage;
34
34
  export type AgentLike<TContext = unknown> = {
35
35
  query(prompt: string | ContentBlock[], options?: QueryOptions<TContext>): AsyncGenerator<AgentLikeEvent>;
36
36
  prompt(prompt: string | ContentBlock[], options?: QueryOptions<TContext>): Promise<SDKResultMessage>;
37
+ /** End the in-flight model request with an "interrupted" result; follow up with a new query(). No-op when idle. */
38
+ interrupt(): void;
37
39
  };
38
40
  export type DelegateWaitMode = "result" | "accepted";
39
41
  export type AgentRuntimeSource = {
@@ -150,6 +152,26 @@ export type ModelMessage = {
150
152
  role: "user" | "assistant";
151
153
  content: string | ContentBlock[];
152
154
  };
155
+ /**
156
+ * Persistence adapter for an Agent's conversation history. `load()` runs once
157
+ * per Agent lifetime, before the first query; `append()` follows every message
158
+ * added to the history; `replace()` follows compaction, which rewrites the
159
+ * whole history — a store must support full replacement. Methods may be
160
+ * synchronous or return a promise; the Agent awaits them to preserve order.
161
+ * Like `ContextTracer`, a failing store is swallowed by default and the
162
+ * conversation continues in memory only; set `failOnError` to propagate store
163
+ * errors out of `query()` instead.
164
+ */
165
+ export type HistoryStore = {
166
+ failOnError?: boolean;
167
+ load(): ModelMessage[] | Promise<ModelMessage[]>;
168
+ append(message: ModelMessage): void | Promise<void>;
169
+ replace(messages: ModelMessage[]): void | Promise<void>;
170
+ };
171
+ export type JsonlHistoryStoreOptions = {
172
+ path: string;
173
+ failOnError?: boolean;
174
+ };
153
175
  export type TokenUsage = {
154
176
  input_tokens: number;
155
177
  output_tokens: number;
@@ -171,6 +193,13 @@ export type AssistantModelMessage = {
171
193
  /** Absent when a custom ModelClient does not report it. */
172
194
  usage?: TokenUsage;
173
195
  stopReason?: StopReason;
196
+ /** The provider-assigned response id. Absent when a custom ModelClient does not report it. */
197
+ providerResponseId?: string;
198
+ /**
199
+ * The model that actually served this response, which may differ from
200
+ * `ModelRequest.model`. Absent when a custom ModelClient does not report it.
201
+ */
202
+ model?: string;
174
203
  };
175
204
  export type ModelToolDefinition = {
176
205
  name: string;
@@ -197,6 +226,8 @@ export interface ModelClient {
197
226
  }
198
227
  export type ToolResult = {
199
228
  content: string | ContentBlock[];
229
+ /** End the run after this tool batch: finish with subtype "success" instead of calling the model again. */
230
+ endTurn?: boolean;
200
231
  };
201
232
  export type ToolKind = "tool" | "agent_tool";
202
233
  export type ToolBatchCall = {
@@ -458,6 +489,8 @@ export type Team = {
458
489
  drain(options?: TeamDrainOptions): Promise<TeamDrainResult>;
459
490
  query(prompt: string | ContentBlock[], options?: QueryOptions): AsyncGenerator<TeamRunnerMessage>;
460
491
  prompt(prompt: string | ContentBlock[], options?: QueryOptions): Promise<SDKResultMessage>;
492
+ /** Interrupts the lead agent's in-flight model request. */
493
+ interrupt(): void;
461
494
  };
462
495
  export type TeamDrainOptions = {
463
496
  maxRounds?: number;
@@ -480,6 +513,8 @@ export type TeamRunner = {
480
513
  mailbox: TeamMailbox;
481
514
  query(prompt: string | ContentBlock[], options?: QueryOptions): AsyncGenerator<TeamRunnerMessage>;
482
515
  prompt(prompt: string | ContentBlock[], options?: QueryOptions): Promise<SDKResultMessage>;
516
+ /** Interrupts the root agent's in-flight model request. */
517
+ interrupt(): void;
483
518
  };
484
519
  export type PermissionRequest = {
485
520
  toolName: string;
@@ -524,6 +559,12 @@ export type AgentOptions<TContext = unknown> = {
524
559
  permission?: (request: PermissionRequest) => Promise<PermissionDecision> | PermissionDecision;
525
560
  modelClient?: ModelClient;
526
561
  tracer?: ContextTracer;
562
+ /**
563
+ * Persistence adapter for the conversation history. Seeded via `load()` once
564
+ * before the first query, then notified on every history write. Compaction
565
+ * calls `replace()`, so the store must support full replacement.
566
+ */
567
+ historyStore?: HistoryStore;
527
568
  };
528
569
  export type BareAgentOptions<TContext = unknown> = Omit<AgentOptions<TContext>, "workspace">;
529
570
  export type AgentWorkspaceToolsOptions = {
@@ -579,6 +620,14 @@ export type SDKAssistantMessage = {
579
620
  message: AssistantModelMessage;
580
621
  session_id: string;
581
622
  };
623
+ /**
624
+ * Emitted only after a whole tool batch finishes — one event per batch, never
625
+ * one per tool, and never for the prompt (the prompt is not echoed; subscribe
626
+ * a `ContextTracer` for a full transcript). `message.content` is always
627
+ * `ToolResultBlock[]`; the declared `ModelMessage` type is wider than this
628
+ * guarantee. `tool_use_result` is a convenience view: a single tool's result
629
+ * `content`, or the array of result blocks when the batch had several tools.
630
+ */
582
631
  export type SDKUserMessage = {
583
632
  type: "user";
584
633
  message: ModelMessage;
@@ -593,7 +642,12 @@ export type SDKStreamEventMessage = {
593
642
  };
594
643
  export type SDKResultMessage = {
595
644
  type: "result";
596
- subtype: "success" | "error" | "error_max_turns" | "error_abort" | "error_timeout";
645
+ /**
646
+ * `"interrupted"` is not an error: `Agent.interrupt()` ends the query this
647
+ * way, keeping completed turns in history so a follow-up query can continue
648
+ * the conversation. `is_error` stays `false` for it.
649
+ */
650
+ subtype: "success" | "interrupted" | "error" | "error_max_turns" | "error_abort" | "error_timeout";
597
651
  is_error: boolean;
598
652
  result: string;
599
653
  session_id: string;
@@ -692,12 +746,33 @@ export type AgentToolOptions = {
692
746
  description: string;
693
747
  targetMailboxId?: string;
694
748
  };
695
- export declare function agentTool(name: string, agent: AgentLike<any>, options: AgentToolOptions): ToolDefinition<AgentToolInput>;
696
- export declare function delegateTool(name: string, description: string, agent: AgentLike<any>, options?: DelegateToolOptions): ToolDefinition<{
749
+ /**
750
+ * An AgentLike is a live session: it keeps its conversation history across
751
+ * calls. An AgentSpec is a template: each call spawns a fresh session with no
752
+ * memory of previous calls. Prefer a spec unless the parent explicitly wants
753
+ * continuity.
754
+ */
755
+ export type AgentToolTarget = AgentLike<any> | AgentSpec<any>;
756
+ export declare function agentTool(name: string, agent: AgentToolTarget, options: AgentToolOptions): ToolDefinition<AgentToolInput>;
757
+ export declare function delegateTool(name: string, description: string, agent: AgentToolTarget, options?: DelegateToolOptions): ToolDefinition<{
697
758
  task: string;
698
759
  }>;
699
760
  export declare function createAgent<TContext = unknown>(options: AgentOptions<TContext>): Agent<TContext>;
700
761
  export declare function createBareAgent<TContext = unknown>(options: BareAgentOptions<TContext>): Agent<TContext>;
762
+ /**
763
+ * A template describing an agent's identity: model, prompt, tools, skills,
764
+ * and workspace policy. A spec carries no conversation state; `spawn()`
765
+ * creates an independent session (an Agent) that owns its own history and
766
+ * workspace. Register a spec wherever a capability should be reused without
767
+ * leaking memory between tasks; spawn a session when continuity is wanted.
768
+ */
769
+ export type AgentSpec<TContext = unknown> = {
770
+ readonly name?: string;
771
+ readonly options: AgentOptions<TContext>;
772
+ spawn(overrides?: Partial<AgentOptions<TContext>>): Agent<TContext>;
773
+ };
774
+ export declare function defineAgent<TContext = unknown>(options: AgentOptions<TContext>): AgentSpec<TContext>;
775
+ export declare function isAgentSpec(target: AgentLike<any> | AgentSpec<any>): target is AgentSpec<any>;
701
776
  export declare function createBuiltinTools(options?: AgentWorkspaceToolsOptions): Array<ToolDefinition<any, any>>;
702
777
  export declare function createJsonlContextTracer(options: JsonlContextTracerOptions): ContextTracer;
703
778
  /**
@@ -713,6 +788,15 @@ export declare function loadSkill(path: string): Promise<SkillDefinition>;
713
788
  export declare function createMCPTools(client: MCPClient, options?: MCPToolsOptions): Promise<Array<ToolDefinition<Record<string, unknown>>>>;
714
789
  export declare function connectMCPStdioServer(server: StdioServerParameters, options?: MCPStdioServerOptions): Promise<MCPStdioConnection>;
715
790
  export declare function connectMCPStreamableHTTPServer(url: string | URL, options?: MCPStreamableHTTPServerOptions): Promise<MCPStreamableHTTPConnection>;
791
+ /**
792
+ * Persists an Agent's history as one JSON message per line. `append()` adds a
793
+ * line; `replace()` rewrites the whole file, which is how compaction is
794
+ * persisted. `load()` reads every line and skips malformed ones — a truncated
795
+ * final line from a torn write must not lose the rest of the transcript.
796
+ * Writes are serialized through an internal queue, so callers may fire them
797
+ * without waiting for ordering.
798
+ */
799
+ export declare function createJsonlHistoryStore(options: JsonlHistoryStoreOptions): HistoryStore;
716
800
  export declare function teamMember(input: TeamMemberInput): TeamMemberDefinition;
717
801
  export declare function createMemoryMailbox(): TeamMailbox;
718
802
  export declare function createSQLiteMailbox(options: SQLiteMailboxOptions): TeamMailbox;
@@ -735,6 +819,8 @@ export declare class Agent<TContext = unknown> {
735
819
  private readonly sessionId;
736
820
  private readonly toolConcurrency;
737
821
  private running;
822
+ private interruptController;
823
+ private historyLoaded;
738
824
  constructor(options: AgentOptions<TContext>);
739
825
  /**
740
826
  * One Agent owns one conversation. Overlapping queries would interleave writes
@@ -742,8 +828,32 @@ export declare class Agent<TContext = unknown> {
742
828
  * a conversation containing someone else's turns.
743
829
  */
744
830
  query(prompt: string | ContentBlock[], options?: QueryOptions<TContext>): AsyncGenerator<SDKMessage>;
831
+ /**
832
+ * Abort the in-flight model request and end the current query with subtype
833
+ * "interrupted". Completed turns stay in history; follow up with a new
834
+ * query() to continue the conversation. Unlike `QueryOptions.signal`, which
835
+ * terminates the query as an error, an interrupt is normal control flow.
836
+ * A no-op when no query is running.
837
+ */
838
+ interrupt(): void;
839
+ /**
840
+ * The store is read once per Agent lifetime, lazily on the first query or
841
+ * getHistory() call — the constructor cannot be async. A failed load follows
842
+ * the same rule as a failed write: swallowed unless `failOnError` is set, in
843
+ * which case the error propagates and the Agent starts empty.
844
+ */
845
+ private ensureHistoryLoaded;
846
+ private loadHistory;
847
+ private appendStoredHistory;
848
+ private replaceStoredHistory;
745
849
  private runQuery;
746
850
  prompt(prompt: string | ContentBlock[], options?: QueryOptions<TContext>): Promise<SDKResultMessage>;
851
+ /**
852
+ * The conversation history as currently held by this Agent, including any
853
+ * messages seeded from `AgentOptions.historyStore`. Returns a deep copy, so
854
+ * mutating the result cannot corrupt the live conversation.
855
+ */
856
+ getHistory(): Promise<ModelMessage[]>;
747
857
  addTools(tools: Array<ToolDefinition<any, TContext>>): void;
748
858
  private initMessage;
749
859
  private resultMessage;