agent-lattice 0.9.24 → 0.12.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
@@ -7,6 +7,11 @@ Install it from npm as `agent-lattice`. It works with Anthropic and
7
7
  Anthropic-compatible providers such as DeepSeek, without installing the Claude
8
8
  Code CLI runtime.
9
9
 
10
+ > **Integrating this SDK from an AI agent?** Start at
11
+ > <https://docs.claude-code-sdk.com/llms.txt> for an index of the documentation,
12
+ > where every page is served as clean Markdown. Read
13
+ > <https://docs.claude-code-sdk.com/llms-full.txt> to take it all in one request.
14
+
10
15
  ## Install
11
16
 
12
17
  ```bash
@@ -50,6 +55,24 @@ const agent = createBareAgent({
50
55
  });
51
56
  ```
52
57
 
58
+ `agent.query()` yields `stream_event` messages while the model is still
59
+ responding, so a host can render output incrementally:
60
+
61
+ ```ts
62
+ for await (const message of agent.query("Say hello")) {
63
+ if (message.type === "stream_event") {
64
+ const event = message.event as { type: string; delta?: { text?: string } };
65
+ if (event.type === "content_block_delta" && event.delta?.text) {
66
+ process.stdout.write(event.delta.text);
67
+ }
68
+ }
69
+ }
70
+ ```
71
+
72
+ The SDK produces the next event only after the loop takes the current one.
73
+ Slow work in the loop body delays later events without dropping or reordering
74
+ them, so keep expensive handling off the loop itself.
75
+
53
76
  Pass `{ stream: false }` to disable model streaming for a query:
54
77
 
55
78
  ```ts
@@ -103,6 +126,30 @@ Use `{ type: "adaptive" }` for models that support adaptive thinking. Use
103
126
  budget is capped at `maxTokens - 1` to satisfy the Anthropic API constraint.
104
127
  When omitted, the SDK does not send a thinking configuration.
105
128
 
129
+ For Kimi K3 through an Anthropic-compatible endpoint or gateway, use
130
+ `reasoningEffort` to send the provider's top-level `reasoning_effort` parameter:
131
+
132
+ ```ts
133
+ const agent = createAgent({
134
+ apiKey: process.env.MOONSHOT_API_KEY,
135
+ baseURL: process.env.KIMI_ANTHROPIC_BASE_URL,
136
+ model: "kimi-k3",
137
+ reasoningEffort: "high",
138
+ });
139
+
140
+ const result = await agent.prompt("Solve this carefully.", {
141
+ reasoningEffort: "low",
142
+ });
143
+ ```
144
+
145
+ The supported values are `"low"`, `"high"`, and `"max"`. A query-level value
146
+ overrides the agent default. When omitted, the SDK does not send
147
+ `reasoning_effort`, so the provider applies its own default (`"max"` for Kimi
148
+ K3). Kimi K3 does not accept `thinkingConfig`; use `reasoningEffort` instead.
149
+ The Kimi Open Platform endpoint at `https://api.moonshot.cn/v1` uses the OpenAI
150
+ Chat Completions protocol and is not a valid `baseURL` for the SDK's built-in
151
+ Anthropic client.
152
+
106
153
  ## Multimodal Input
107
154
 
108
155
  Pass Anthropic-compatible content blocks for image or document prompts:
@@ -230,9 +277,13 @@ const tracer = createLangSmithContextTracer({
230
277
  });
231
278
  ```
232
279
 
233
- LangSmith receives one root `chain` run per SDK query, child `llm` runs for
234
- model turns, child `tool` runs for SDK tool calls, and run events for auxiliary
235
- trace events.
280
+ LangSmith receives one root `chain` run per SDK query. For an `Agent` query,
281
+ model turns and SDK tool calls appear as child `llm` and `tool` runs. For a
282
+ `Team` query, the root represents the complete Team invocation; the initial
283
+ Lead run, delegated Member runs, and later Lead runs all appear beneath that
284
+ root and share one trace session. Each Agent keeps its own SDK session identity,
285
+ recorded as `agent_session_id` metadata, so tracing does not change Agent state
286
+ or returned SDK messages.
236
287
 
237
288
  Custom sinks can implement the same interface for SQLite, OpenTelemetry, object
238
289
  storage, or host-specific observability. The functions below are application
@@ -283,6 +334,47 @@ const result = await agent.prompt("What is 2+2?");
283
334
  console.log(result.result);
284
335
  ```
285
336
 
337
+ ## Concurrent Tool Calls
338
+
339
+ The model requests concurrency by returning multiple `tool_use` blocks in one
340
+ assistant message. The SDK makes the final safety decision. By default, only
341
+ tools whose parsed input passes `isConcurrencySafe(input)` run together:
342
+
343
+ ```ts
344
+ const search = tool(
345
+ "search",
346
+ "Search documents",
347
+ z.object({ query: z.string() }),
348
+ async ({ query }) => {
349
+ // App code: replace with your database or search client.
350
+ return { content: await documentIndex.search(query) };
351
+ },
352
+ { isConcurrencySafe: () => true },
353
+ );
354
+
355
+ const agent = createAgent({
356
+ model: "claude-sonnet-4-6",
357
+ tools: [search],
358
+ toolConcurrency: { mode: "safe", maxConcurrency: 8 },
359
+ });
360
+ ```
361
+
362
+ `safe` is the default mode, `maxConcurrency` defaults to `10`, and tools without
363
+ an `isConcurrencySafe` declaration stay sequential. Use `mode: "all"` only when
364
+ every tool in the Agent is safe to overlap. Use `mode: "sequential"` to disable
365
+ tool concurrency even for tools marked safe.
366
+
367
+ When concurrency is available, the SDK tells the model to batch independent
368
+ calls and to use separate assistant responses when a later call needs an earlier
369
+ result. Runtime safety checks and `toolBatchPolicy` remain authoritative.
370
+
371
+ The SDK waits for the complete batch before requesting the model again. Tools
372
+ may finish in any order, while the `tool_result` blocks sent to the model remain
373
+ in the original `tool_use` order. One tool failure does not discard the other
374
+ results. On abort, running handlers receive the shared `AbortSignal`, queued
375
+ handlers do not start, and the SDK waits for handlers that already started to
376
+ settle.
377
+
286
378
  ## Tool Batch Policy
287
379
 
288
380
  Use `toolBatchPolicy` when some tools must not run in the same model response.
@@ -324,6 +416,44 @@ policy, tool execution is unchanged. A policy prevents known bad combinations
324
416
  inside one model response, but it does not replace database transactions or
325
417
  revision checks against concurrent external updates.
326
418
 
419
+ ## Hooks
420
+
421
+ `permission` and `toolBatchPolicy` decide whether something runs. Hooks decide
422
+ what it looks like — redacting tool output, trimming context before a request,
423
+ or injecting retrieved documents:
424
+
425
+ ```ts
426
+ const agent = createAgent({
427
+ apiKey: process.env.ANTHROPIC_API_KEY,
428
+ model: "claude-sonnet-4-6",
429
+ tools: [queryDatabase],
430
+ hooks: {
431
+ async onToolResult({ toolName, result, error }) {
432
+ if (toolName !== "queryDatabase") return; // undefined: leave unchanged
433
+ return { ...result, content: await redact(result.content) };
434
+ },
435
+ onModelRequest({ messages, turn }) {
436
+ if (messages.length < 40) return;
437
+ return { messages: compact(messages) };
438
+ },
439
+ },
440
+ });
441
+ ```
442
+
443
+ `onToolResult` sees every result on its way to the model, including handler
444
+ failures, aborted calls, and calls blocked by `toolBatchPolicy`. `onModelRequest`
445
+ shapes a single request; the stored conversation is untouched, so trimming
446
+ context does not destroy history.
447
+
448
+ A hook returns a replacement or nothing, and must not mutate what it receives. A
449
+ hook that throws propagates out of `query()` rather than becoming an error
450
+ `result` — a redaction hook that failed quietly would leak the data it exists to
451
+ protect. Hooks run before the matching trace event, so traces record what was
452
+ actually sent.
453
+
454
+ Compose independent concerns with `createCompositeAgentHooks([a, b, c])`, which
455
+ chains them in order, each receiving the previous one's output.
456
+
327
457
  ## Business Context For Tools
328
458
 
329
459
  Pass host application data through `context`. The SDK gives that context to
@@ -852,3 +982,62 @@ console.log(result.result);
852
982
  The SDK stores conversation state in memory for the lifetime of the `Agent`
853
983
  instance. Persistent transcripts and resume support are intentionally out of
854
984
  scope for the first release.
985
+
986
+ An `Agent` is a conversation, not a reusable client. Because the history is
987
+ instance state, starting a query while another is still running would interleave
988
+ both conversations; the SDK rejects the second one with `ConcurrentQueryError`.
989
+ Create one Agent per concurrent conversation — in a server, per request or per
990
+ user session rather than a shared module-level instance. Sequential reuse, as
991
+ above, is the intended pattern.
992
+
993
+ ## Deadlines
994
+
995
+ `QueryOptions.signal` bounds a whole query — every model request, tool call, and
996
+ turn together. `requestTimeoutMs` bounds each single model request, so an agent
997
+ that legitimately runs many tool-using turns does not have to fit them all into
998
+ one budget:
999
+
1000
+ ```ts
1001
+ const agent = createAgent({
1002
+ apiKey: process.env.ANTHROPIC_API_KEY,
1003
+ model: "claude-sonnet-4-6",
1004
+ requestTimeoutMs: 120_000,
1005
+ });
1006
+
1007
+ const result = await agent.prompt("Audit this repository.", {
1008
+ signal: AbortSignal.timeout(600_000),
1009
+ requestTimeoutMs: 60_000, // overrides the agent default for this query
1010
+ });
1011
+ ```
1012
+
1013
+ A request deadline produces `subtype: "error_timeout"` with a `TimeoutError`,
1014
+ distinct from the `"error_abort"` of a caller-initiated cancellation, so hosts
1015
+ can retry timeouts without retrying deliberate cancellations.
1016
+
1017
+ Both limits are enforced by the SDK rather than delegated. `ModelRequest` carries
1018
+ `signal` and `timeoutMs` so a client can cancel its own work, but the agent loop
1019
+ also races the call, so a `ModelClient` that honours neither cannot stall the
1020
+ loop indefinitely. Losing that race abandons the call rather than cancelling it.
1021
+
1022
+ ## Token Usage And Truncation
1023
+
1024
+ Every `result` message reports `usage`, summed over the model requests in that
1025
+ query, plus the `stop_reason` of the last response:
1026
+
1027
+ ```ts
1028
+ const result = await agent.prompt("Summarize this file.");
1029
+ console.log(result.usage);
1030
+ // { input_tokens: 1200, output_tokens: 512, cache_read_input_tokens: 800 }
1031
+
1032
+ if (result.stop_reason === "max_tokens") {
1033
+ // subtype is still "success", but result is a fragment, not an answer.
1034
+ }
1035
+ ```
1036
+
1037
+ `stop_reason: "max_tokens"` means the model hit its output budget mid-response.
1038
+ The SDK does not treat that as an error, so checking this field is the only way
1039
+ to distinguish a complete answer from a truncated one.
1040
+
1041
+ Usage comes from the model client. The built-in Anthropic client fills it in from
1042
+ the response, including the streaming path; a custom `ModelClient` that omits
1043
+ `usage` produces zeroed counts rather than an error.
package/dist/index.d.ts CHANGED
@@ -150,9 +150,23 @@ export type ModelMessage = {
150
150
  role: "user" | "assistant";
151
151
  content: string | ContentBlock[];
152
152
  };
153
+ export type TokenUsage = {
154
+ input_tokens: number;
155
+ output_tokens: number;
156
+ cache_creation_input_tokens?: number;
157
+ cache_read_input_tokens?: number;
158
+ };
159
+ /**
160
+ * Why the model stopped. `"max_tokens"` means the response was cut off mid-way:
161
+ * the text is a fragment, not an answer. Left open because providers add values.
162
+ */
163
+ export type StopReason = "end_turn" | "max_tokens" | "stop_sequence" | "tool_use" | "pause_turn" | "refusal" | (string & {});
153
164
  export type AssistantModelMessage = {
154
165
  role: "assistant";
155
166
  content: ContentBlock[];
167
+ /** Absent when a custom ModelClient does not report it. */
168
+ usage?: TokenUsage;
169
+ stopReason?: StopReason;
156
170
  };
157
171
  export type ModelToolDefinition = {
158
172
  name: string;
@@ -168,6 +182,9 @@ export type ModelRequest = {
168
182
  stream: boolean;
169
183
  outputFormat?: OutputFormat;
170
184
  thinkingConfig?: ThinkingConfig;
185
+ reasoningEffort?: ReasoningEffort;
186
+ /** Deadline for this single request. Clients should honour it; the SDK also enforces it. */
187
+ timeoutMs?: number;
171
188
  onStreamEvent?: (event: Record<string, unknown>) => void;
172
189
  signal?: AbortSignal;
173
190
  };
@@ -210,7 +227,58 @@ export type ToolExecutionContext<TContext = unknown> = {
210
227
  agentRuntime?: AgentRuntimeContext;
211
228
  permissions?: RuntimePermissions;
212
229
  };
230
+ export type ToolResultHookContext<TContext = unknown> = {
231
+ toolName: string;
232
+ toolUseId: string;
233
+ /** Raw input as the model sent it, before the tool schema parsed it. */
234
+ input: unknown;
235
+ /** What the SDK would send back to the model. */
236
+ result: ToolResultBlock;
237
+ /** Present when the handler failed, was denied, or was cancelled. */
238
+ error?: Error;
239
+ context?: TContext;
240
+ source?: AgentRuntimeSource;
241
+ signal?: AbortSignal;
242
+ };
243
+ export type ModelRequestHookContext<TContext = unknown> = {
244
+ /** What the SDK would send. Not the stored history; see AgentHooks. */
245
+ messages: ModelMessage[];
246
+ systemPrompt?: string;
247
+ /** 1 for the first model request of the query. */
248
+ turn: number;
249
+ context?: TContext;
250
+ source?: AgentRuntimeSource;
251
+ signal?: AbortSignal;
252
+ };
253
+ export type ModelRequestHookResult = {
254
+ messages?: ModelMessage[];
255
+ systemPrompt?: string;
256
+ };
257
+ /**
258
+ * Lifecycle callbacks that can rewrite what crosses the agent loop's boundaries,
259
+ * as opposed to `permission` and `toolBatchPolicy`, which can only allow or deny.
260
+ *
261
+ * A hook returns a replacement, or nothing to leave the value unchanged; it must
262
+ * not mutate what it receives.
263
+ *
264
+ * A hook that throws propagates out of `query()` rather than being swallowed the
265
+ * way a tracer error is, or being reported as an error `result`. A hook failure
266
+ * is host code failing, like `ConcurrentQueryError`, not an upstream failure the
267
+ * loop can describe to the model — and a redaction hook that failed quietly
268
+ * would leak the data it exists to protect.
269
+ *
270
+ * Hooks run before the matching trace event, so traces record what was actually
271
+ * sent. `onModelRequest` shapes one request only and never edits the stored
272
+ * conversation, so trimming context for a long turn does not destroy history.
273
+ */
274
+ export type AgentHooks<TContext = unknown> = {
275
+ onToolResult?(context: ToolResultHookContext<TContext>): ToolResultBlock | void | Promise<ToolResultBlock | void>;
276
+ onModelRequest?(context: ModelRequestHookContext<TContext>): ModelRequestHookResult | void | Promise<ModelRequestHookResult | void>;
277
+ };
213
278
  export type ToolHandler<TInput = unknown, TContext = unknown> = (input: TInput, context: ToolExecutionContext<TContext>) => Promise<ToolResult> | ToolResult;
279
+ export type ToolOptions<TInput = unknown> = {
280
+ isConcurrencySafe?: (input: TInput) => boolean;
281
+ };
214
282
  export type ToolDefinition<TInput = unknown, TContext = unknown> = {
215
283
  name: string;
216
284
  description: string;
@@ -219,6 +287,12 @@ export type ToolDefinition<TInput = unknown, TContext = unknown> = {
219
287
  jsonSchema: Record<string, unknown>;
220
288
  parse(input: unknown): TInput;
221
289
  handler: ToolHandler<TInput, TContext>;
290
+ isConcurrencySafe?: (input: TInput) => boolean;
291
+ };
292
+ export type ToolConcurrencyMode = "safe" | "all" | "sequential";
293
+ export type ToolConcurrencyOptions = {
294
+ mode?: ToolConcurrencyMode;
295
+ maxConcurrency?: number;
222
296
  };
223
297
  export type SkillDefinition = {
224
298
  name: string;
@@ -412,8 +486,14 @@ export type AgentOptions<TContext = unknown> = {
412
486
  maxTokens?: number;
413
487
  maxTurns?: number;
414
488
  thinkingConfig?: ThinkingConfig;
489
+ reasoningEffort?: ReasoningEffort;
490
+ /** Deadline for each single model request, in milliseconds. Unset means no SDK-side limit. */
491
+ requestTimeoutMs?: number;
415
492
  tools?: Array<ToolDefinition<any, TContext>>;
416
493
  toolBatchPolicy?: ToolBatchPolicy<TContext>;
494
+ /** Lifecycle callbacks that rewrite tool results and outgoing model requests. */
495
+ hooks?: AgentHooks<TContext>;
496
+ toolConcurrency?: ToolConcurrencyOptions;
417
497
  skills?: SkillDefinition[];
418
498
  workspace?: AgentWorkspaceOptions;
419
499
  permission?: (request: PermissionRequest) => Promise<PermissionDecision> | PermissionDecision;
@@ -430,6 +510,9 @@ export type QueryOptions<TContext = unknown> = {
430
510
  stream?: boolean;
431
511
  outputFormat?: OutputFormat;
432
512
  thinkingConfig?: ThinkingConfig;
513
+ reasoningEffort?: ReasoningEffort;
514
+ /** Overrides the agent's per-request deadline for this query. */
515
+ requestTimeoutMs?: number;
433
516
  signal?: AbortSignal;
434
517
  context?: TContext;
435
518
  agentRuntime?: AgentRuntimeContext;
@@ -445,6 +528,7 @@ export type ThinkingConfig = {
445
528
  } | {
446
529
  type: "disabled";
447
530
  };
531
+ export type ReasoningEffort = "low" | "high" | "max";
448
532
  export type SDKSystemInitMessage = {
449
533
  type: "system";
450
534
  subtype: "init";
@@ -471,12 +555,19 @@ export type SDKStreamEventMessage = {
471
555
  };
472
556
  export type SDKResultMessage = {
473
557
  type: "result";
474
- subtype: "success" | "error" | "error_max_turns" | "error_abort";
558
+ subtype: "success" | "error" | "error_max_turns" | "error_abort" | "error_timeout";
475
559
  is_error: boolean;
476
560
  result: string;
477
561
  session_id: string;
478
562
  num_turns: number;
479
563
  error?: Error;
564
+ /** Summed over every model request in the query. Zeroed when unreported. */
565
+ usage: TokenUsage;
566
+ /**
567
+ * From the last model response. Check for `"max_tokens"`: `subtype` is still
568
+ * `"success"` there, but `result` is a truncated fragment.
569
+ */
570
+ stop_reason?: StopReason;
480
571
  };
481
572
  export type SDKMessage = SDKSystemInitMessage | SDKStreamEventMessage | SDKAssistantMessage | SDKUserMessage | SDKResultMessage;
482
573
  export type TeamRunnerSource = AgentRuntimeSource;
@@ -518,6 +609,12 @@ export declare class MaxTurnsError extends AgentSDKError {
518
609
  }
519
610
  export declare class AbortError extends AgentSDKError {
520
611
  }
612
+ /** A second query was started on an Agent that was still running one. */
613
+ export declare class ConcurrentQueryError extends AgentSDKError {
614
+ }
615
+ /** A model request exceeded `requestTimeoutMs`. */
616
+ export declare class TimeoutError extends AgentSDKError {
617
+ }
521
618
  export declare class ToolBatchRejectedError extends AgentSDKError {
522
619
  readonly rejection: ToolBatchPolicyRejection;
523
620
  constructor(rejection: ToolBatchPolicyRejection);
@@ -526,8 +623,8 @@ export declare class ToolPermissionDeniedError extends AgentSDKError {
526
623
  readonly denial: PermissionDenial;
527
624
  constructor(denial: PermissionDenial);
528
625
  }
529
- export declare function tool<TContext = unknown>(): <TSchema>(name: string, description: string, inputSchema: TSchema, handler: ToolHandler<InferInput<TSchema>, TContext>) => ToolDefinition<InferInput<TSchema>, TContext>;
530
- export declare function tool<TSchema, TContext = unknown>(name: string, description: string, inputSchema: TSchema, handler: ToolHandler<InferInput<TSchema>, TContext>): ToolDefinition<InferInput<TSchema>, TContext>;
626
+ export declare function tool<TContext = unknown>(): <TSchema>(name: string, description: string, inputSchema: TSchema, handler: ToolHandler<InferInput<TSchema>, TContext>, options?: ToolOptions<InferInput<TSchema>>) => ToolDefinition<InferInput<TSchema>, TContext>;
627
+ export declare function tool<TSchema, TContext = unknown>(name: string, description: string, inputSchema: TSchema, handler: ToolHandler<InferInput<TSchema>, TContext>, options?: ToolOptions<InferInput<TSchema>>): ToolDefinition<InferInput<TSchema>, TContext>;
531
628
  export type DelegateToolOptions = {
532
629
  wait?: DelegateWaitMode;
533
630
  targetMailboxId?: string;
@@ -565,6 +662,12 @@ export declare function createAgent<TContext = unknown>(options: AgentOptions<TC
565
662
  export declare function createBareAgent<TContext = unknown>(options: BareAgentOptions<TContext>): Agent<TContext>;
566
663
  export declare function createBuiltinTools(options?: AgentWorkspaceToolsOptions): Array<ToolDefinition<any, any>>;
567
664
  export declare function createJsonlContextTracer(options: JsonlContextTracerOptions): ContextTracer;
665
+ /**
666
+ * Chains hooks in array order: each one sees the previous one's output, so
667
+ * redaction, truncation, and auditing can be written separately and combined.
668
+ * Unlike the composite tracer, a failure is not swallowed — see `AgentHooks`.
669
+ */
670
+ export declare function createCompositeAgentHooks<TContext = unknown>(hooks: Array<AgentHooks<TContext> | undefined | null>): AgentHooks<TContext>;
568
671
  export declare function createCompositeContextTracer(tracers: Array<ContextTracer | undefined | null>): ContextTracer;
569
672
  export declare function createLangSmithContextTracer(options: LangSmithContextTracerOptions): ContextTracer;
570
673
  export declare function skill(input: SkillInput): SkillDefinition;
@@ -583,6 +686,7 @@ export declare function query<TContext = unknown>(params: AgentOptions<TContext>
583
686
  stream?: boolean;
584
687
  outputFormat?: OutputFormat;
585
688
  thinkingConfig?: ThinkingConfig;
689
+ reasoningEffort?: ReasoningEffort;
586
690
  signal?: AbortSignal;
587
691
  context?: TContext;
588
692
  }): AsyncGenerator<SDKMessage>;
@@ -591,8 +695,16 @@ export declare class Agent<TContext = unknown> {
591
695
  private readonly modelClient;
592
696
  private readonly messages;
593
697
  private readonly sessionId;
698
+ private readonly toolConcurrency;
699
+ private running;
594
700
  constructor(options: AgentOptions<TContext>);
701
+ /**
702
+ * One Agent owns one conversation. Overlapping queries would interleave writes
703
+ * into the shared history, so the second caller is rejected rather than served
704
+ * a conversation containing someone else's turns.
705
+ */
595
706
  query(prompt: string | ContentBlock[], options?: QueryOptions<TContext>): AsyncGenerator<SDKMessage>;
707
+ private runQuery;
596
708
  prompt(prompt: string | ContentBlock[], options?: QueryOptions<TContext>): Promise<SDKResultMessage>;
597
709
  addTools(tools: Array<ToolDefinition<any, TContext>>): void;
598
710
  private initMessage;
@@ -601,6 +713,8 @@ export declare class Agent<TContext = unknown> {
601
713
  private messagesForModel;
602
714
  private selectSkills;
603
715
  private runTool;
716
+ private executeToolBatch;
717
+ private isToolCallConcurrencySafe;
604
718
  }
605
719
  type InferInput<TSchema> = TSchema extends {
606
720
  parse(input: unknown): infer TInput;