agent-lattice 0.15.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,9 +730,14 @@ 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
 
@@ -1074,8 +1115,8 @@ console.log(result.result);
1074
1115
  ```
1075
1116
 
1076
1117
  The SDK stores conversation state in memory for the lifetime of the `Agent`
1077
- instance. Persistent transcripts and resume support are intentionally out of
1078
- 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).
1079
1120
 
1080
1121
  An `Agent` is a conversation, not a reusable client. Because the history is
1081
1122
  instance state, starting a query while another is still running would interleave
@@ -1084,6 +1125,54 @@ Create one Agent per concurrent conversation — in a server, per request or per
1084
1125
  user session rather than a shared module-level instance. Sequential reuse, as
1085
1126
  above, is the intended pattern.
1086
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
+
1087
1176
  ## Deadlines
1088
1177
 
1089
1178
  `QueryOptions.signal` bounds a whole query — every model request, tool call, and
@@ -1113,6 +1202,32 @@ Both limits are enforced by the SDK rather than delegated. `ModelRequest` carrie
1113
1202
  also races the call, so a `ModelClient` that honours neither cannot stall the
1114
1203
  loop indefinitely. Losing that race abandons the call rather than cancelling it.
1115
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
+
1116
1231
  ## Token Usage And Truncation
1117
1232
 
1118
1233
  Every `result` message reports `usage`, summed over the model requests in that
@@ -1135,3 +1250,13 @@ to distinguish a complete answer from a truncated one.
1135
1250
  Usage comes from the model client. The built-in Anthropic client fills it in from
1136
1251
  the response, including the streaming path; a custom `ModelClient` that omits
1137
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;
@@ -734,6 +788,15 @@ export declare function loadSkill(path: string): Promise<SkillDefinition>;
734
788
  export declare function createMCPTools(client: MCPClient, options?: MCPToolsOptions): Promise<Array<ToolDefinition<Record<string, unknown>>>>;
735
789
  export declare function connectMCPStdioServer(server: StdioServerParameters, options?: MCPStdioServerOptions): Promise<MCPStdioConnection>;
736
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;
737
800
  export declare function teamMember(input: TeamMemberInput): TeamMemberDefinition;
738
801
  export declare function createMemoryMailbox(): TeamMailbox;
739
802
  export declare function createSQLiteMailbox(options: SQLiteMailboxOptions): TeamMailbox;
@@ -756,6 +819,8 @@ export declare class Agent<TContext = unknown> {
756
819
  private readonly sessionId;
757
820
  private readonly toolConcurrency;
758
821
  private running;
822
+ private interruptController;
823
+ private historyLoaded;
759
824
  constructor(options: AgentOptions<TContext>);
760
825
  /**
761
826
  * One Agent owns one conversation. Overlapping queries would interleave writes
@@ -763,8 +828,32 @@ export declare class Agent<TContext = unknown> {
763
828
  * a conversation containing someone else's turns.
764
829
  */
765
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;
766
849
  private runQuery;
767
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[]>;
768
857
  addTools(tools: Array<ToolDefinition<any, TContext>>): void;
769
858
  private initMessage;
770
859
  private resultMessage;