agent-lattice 0.12.0 → 0.15.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
@@ -416,6 +416,62 @@ policy, tool execution is unchanged. A policy prevents known bad combinations
416
416
  inside one model response, but it does not replace database transactions or
417
417
  revision checks against concurrent external updates.
418
418
 
419
+ ## Automatic Context Compaction
420
+
421
+ History only grows, so a long-running agent eventually exceeds the model's
422
+ context window. Enable `autoCompact` to replace the older part of the
423
+ conversation with a model-written summary:
424
+
425
+ ```ts
426
+ const agent = createAgent({
427
+ apiKey: process.env.ANTHROPIC_API_KEY,
428
+ model: "claude-sonnet-4-6",
429
+ autoCompact: true, // or { thresholdTokens: 150_000, keepRecentMessages: 8 }
430
+ });
431
+ ```
432
+
433
+ Compaction runs between turns, once a response reports more input tokens than
434
+ `thresholdTokens` (default `100000`). Everything except the last
435
+ `keepRecentMessages` messages (default `6`) is summarized, and the history is
436
+ rebuilt as that summary followed by the retained messages. The summary is
437
+ wrapped in an instruction telling the model that compaction just happened and to
438
+ continue from it, so the next turn resumes the task instead of restarting it.
439
+
440
+ Unlike the `onModelRequest` hook, which shapes a single request, this **rewrites
441
+ the stored conversation** — that is what makes the saving persist, but the
442
+ replaced turns are gone.
443
+
444
+ The cut point never separates a `tool_result` from the `tool_use` that produced
445
+ it, because the model API rejects that. If no safe cut leaves anything to
446
+ summarize, compaction is skipped.
447
+
448
+ Compaction costs a model call. Its tokens are folded into `result.usage`, and a
449
+ `system` message with `subtype: "compaction"` reports what happened:
450
+
451
+ ```ts
452
+ for await (const message of agent.query("Refactor this module.")) {
453
+ if (message.type === "system" && message.subtype === "compaction") {
454
+ console.log(`compacted ${message.compacted_messages} messages`, message.usage);
455
+ }
456
+ }
457
+ ```
458
+
459
+ The trigger depends on reported usage, so a custom `ModelClient` that omits
460
+ `usage` never compacts. Override the instruction with `prompt`, or read the
461
+ built-in one from `DEFAULT_COMPACTION_PROMPT`.
462
+
463
+ The threshold is a forecast, so a single large tool result can still carry a
464
+ request past the window. Compaction then runs as a recovery — summarize, then
465
+ retry the same turn — on either `stop_reason: "model_context_window_exceeded"`
466
+ or an API error naming a too-long prompt. It is attempted once per query; if
467
+ summarizing fails or there is nothing left to summarize, the original failure
468
+ surfaces unchanged.
469
+
470
+ `stop_reason: "max_tokens"` deliberately does **not** trigger compaction. It
471
+ means the *output* hit `maxTokens`, not that the input was too large — the model
472
+ had room to read and ran out of room to write, so compacting the history would
473
+ not make the answer complete. Raise `maxTokens` instead.
474
+
419
475
  ## Hooks
420
476
 
421
477
  `permission` and `toolBatchPolicy` decide whether something runs. Hooks decide
@@ -644,6 +700,44 @@ type AgentLike<TContext = unknown> = {
644
700
  That means a team can be used anywhere a callable agent is expected. From the
645
701
  outside, a team is an agent; inside, it can contain a whole organization.
646
702
 
703
+ ## Agent Specs (Templates) And Sessions
704
+
705
+ *Requires 0.15.0 or later.*
706
+
707
+ `createAgent()` returns a live session: one conversation, one history, one
708
+ workspace. `defineAgent()` returns an `AgentSpec` — a template carrying the
709
+ same options but no state. `spawn()` creates an independent session from it:
710
+
711
+ ```ts
712
+ import { agentTool, defineAgent } from "agent-lattice";
713
+
714
+ const reviewerSpec = defineAgent({
715
+ name: "reviewer",
716
+ model: "claude-sonnet-4-5",
717
+ systemPrompt: "You are a senior code reviewer...",
718
+ });
719
+
720
+ // Register the spec: every tool call spawns a fresh session with no memory
721
+ // of previous calls. This is the safe default for reuse.
722
+ const lead = createAgent({
723
+ model: "claude-sonnet-4-5",
724
+ tools: [
725
+ agentTool("review", reviewerSpec, {
726
+ description: "Ask the reviewer to audit a change.",
727
+ }),
728
+ ],
729
+ });
730
+
731
+ // Register a spawned session instead when the target should remember earlier
732
+ // tasks across calls — continuity is an explicit opt-in.
733
+ const reviewSession = reviewerSpec.spawn();
734
+ ```
735
+
736
+ The same union applies to `delegateTool()`. The generated tool description
737
+ states which semantics a target has, so the calling agent knows whether each
738
+ task must be self-contained. Existing code that passes an `AgentLike` keeps
739
+ its current behavior: a long-lived session with history.
740
+
647
741
  ## Team Mailbox Collaboration
648
742
 
649
743
  Use `createTeam()` when you want to talk to one `AgentLike` while it coordinates
package/dist/index.d.ts CHANGED
@@ -102,7 +102,7 @@ export type AgentRuntimeContext = {
102
102
  emit(message: TeamRunnerMessage): void;
103
103
  shouldPauseAfterToolBatch?(): boolean;
104
104
  };
105
- export type ContextTraceEventType = "run_start" | "user_message" | "model_request" | "assistant_message" | "tool_use" | "tool_result" | "team_message" | "result" | "error";
105
+ export type ContextTraceEventType = "run_start" | "user_message" | "model_request" | "assistant_message" | "tool_use" | "tool_result" | "team_message" | "result" | "compaction" | "error";
106
106
  export type ContextTraceEvent = {
107
107
  version: 1;
108
108
  timestamp: string;
@@ -160,7 +160,11 @@ export type TokenUsage = {
160
160
  * Why the model stopped. `"max_tokens"` means the response was cut off mid-way:
161
161
  * the text is a fragment, not an answer. Left open because providers add values.
162
162
  */
163
- export type StopReason = "end_turn" | "max_tokens" | "stop_sequence" | "tool_use" | "pause_turn" | "refusal" | (string & {});
163
+ export type StopReason = "end_turn"
164
+ /** Output hit `maxTokens`. The text is a fragment; compaction does not help. */
165
+ | "max_tokens" | "stop_sequence" | "tool_use" | "pause_turn" | "refusal"
166
+ /** The context window ran out mid-request. This is what compaction is for. */
167
+ | "model_context_window_exceeded" | (string & {});
164
168
  export type AssistantModelMessage = {
165
169
  role: "assistant";
166
170
  content: ContentBlock[];
@@ -254,6 +258,22 @@ export type ModelRequestHookResult = {
254
258
  messages?: ModelMessage[];
255
259
  systemPrompt?: string;
256
260
  };
261
+ export declare const DEFAULT_COMPACTION_PROMPT = "Your task is to create a detailed summary of the conversation so far,\npaying close attention to the user's explicit requests and your previous actions.\n\nThis summary should be thorough in capturing:\n- technical details\n- code patterns\n- architectural decisions\n- files that were modified\n- commands that were run\n- errors encountered\n- solutions attempted\n- important context needed to continue the work\n\nPreserve:\n- user's intent\n- important constraints\n- decisions already made\n- reasoning behind decisions\n- unresolved issues\n- next steps\n\nThe summary will replace the conversation history, so include everything\nnecessary for another Claude instance to continue the task successfully.\n\nOutput only the summary.";
262
+ export type AutoCompactOptions = {
263
+ /**
264
+ * Compact once a model response reports more input tokens than this.
265
+ * Defaults to 100000, chosen to leave headroom on a 200k-token model.
266
+ */
267
+ thresholdTokens?: number;
268
+ /** Trailing messages left verbatim after the summary. Defaults to 6. */
269
+ keepRecentMessages?: number;
270
+ /** Replaces DEFAULT_COMPACTION_PROMPT. */
271
+ prompt?: string;
272
+ /** Model used for the summary. Defaults to the agent's model. */
273
+ model?: string;
274
+ /** Output cap for the summary. Defaults to 8192. */
275
+ maxTokens?: number;
276
+ };
257
277
  /**
258
278
  * Lifecycle callbacks that can rewrite what crosses the agent loop's boundaries,
259
279
  * as opposed to `permission` and `toolBatchPolicy`, which can only allow or deny.
@@ -493,6 +513,11 @@ export type AgentOptions<TContext = unknown> = {
493
513
  toolBatchPolicy?: ToolBatchPolicy<TContext>;
494
514
  /** Lifecycle callbacks that rewrite tool results and outgoing model requests. */
495
515
  hooks?: AgentHooks<TContext>;
516
+ /**
517
+ * Replaces older conversation history with a model-written summary once it
518
+ * grows past a threshold. Off unless set; `true` uses the defaults.
519
+ */
520
+ autoCompact?: boolean | AutoCompactOptions;
496
521
  toolConcurrency?: ToolConcurrencyOptions;
497
522
  skills?: SkillDefinition[];
498
523
  workspace?: AgentWorkspaceOptions;
@@ -536,6 +561,19 @@ export type SDKSystemInitMessage = {
536
561
  tools: string[];
537
562
  session_id: string;
538
563
  };
564
+ export type SDKSystemCompactionMessage = {
565
+ type: "system";
566
+ subtype: "compaction";
567
+ session_id: string;
568
+ /** Messages replaced by the summary. */
569
+ compacted_messages: number;
570
+ /** Messages kept verbatim after it. */
571
+ retained_messages: number;
572
+ /** Input tokens of the turn that triggered compaction. */
573
+ trigger_input_tokens: number;
574
+ /** Cost of producing the summary, already folded into the result usage. */
575
+ usage: TokenUsage;
576
+ };
539
577
  export type SDKAssistantMessage = {
540
578
  type: "assistant";
541
579
  message: AssistantModelMessage;
@@ -569,7 +607,7 @@ export type SDKResultMessage = {
569
607
  */
570
608
  stop_reason?: StopReason;
571
609
  };
572
- export type SDKMessage = SDKSystemInitMessage | SDKStreamEventMessage | SDKAssistantMessage | SDKUserMessage | SDKResultMessage;
610
+ export type SDKMessage = SDKSystemInitMessage | SDKSystemCompactionMessage | SDKStreamEventMessage | SDKAssistantMessage | SDKUserMessage | SDKResultMessage;
573
611
  export type TeamRunnerSource = AgentRuntimeSource;
574
612
  export type TeamRunnerTeamMessage = {
575
613
  type: "team_message";
@@ -654,12 +692,33 @@ export type AgentToolOptions = {
654
692
  description: string;
655
693
  targetMailboxId?: string;
656
694
  };
657
- export declare function agentTool(name: string, agent: AgentLike<any>, options: AgentToolOptions): ToolDefinition<AgentToolInput>;
658
- export declare function delegateTool(name: string, description: string, agent: AgentLike<any>, options?: DelegateToolOptions): ToolDefinition<{
695
+ /**
696
+ * An AgentLike is a live session: it keeps its conversation history across
697
+ * calls. An AgentSpec is a template: each call spawns a fresh session with no
698
+ * memory of previous calls. Prefer a spec unless the parent explicitly wants
699
+ * continuity.
700
+ */
701
+ export type AgentToolTarget = AgentLike<any> | AgentSpec<any>;
702
+ export declare function agentTool(name: string, agent: AgentToolTarget, options: AgentToolOptions): ToolDefinition<AgentToolInput>;
703
+ export declare function delegateTool(name: string, description: string, agent: AgentToolTarget, options?: DelegateToolOptions): ToolDefinition<{
659
704
  task: string;
660
705
  }>;
661
706
  export declare function createAgent<TContext = unknown>(options: AgentOptions<TContext>): Agent<TContext>;
662
707
  export declare function createBareAgent<TContext = unknown>(options: BareAgentOptions<TContext>): Agent<TContext>;
708
+ /**
709
+ * A template describing an agent's identity: model, prompt, tools, skills,
710
+ * and workspace policy. A spec carries no conversation state; `spawn()`
711
+ * creates an independent session (an Agent) that owns its own history and
712
+ * workspace. Register a spec wherever a capability should be reused without
713
+ * leaking memory between tasks; spawn a session when continuity is wanted.
714
+ */
715
+ export type AgentSpec<TContext = unknown> = {
716
+ readonly name?: string;
717
+ readonly options: AgentOptions<TContext>;
718
+ spawn(overrides?: Partial<AgentOptions<TContext>>): Agent<TContext>;
719
+ };
720
+ export declare function defineAgent<TContext = unknown>(options: AgentOptions<TContext>): AgentSpec<TContext>;
721
+ export declare function isAgentSpec(target: AgentLike<any> | AgentSpec<any>): target is AgentSpec<any>;
663
722
  export declare function createBuiltinTools(options?: AgentWorkspaceToolsOptions): Array<ToolDefinition<any, any>>;
664
723
  export declare function createJsonlContextTracer(options: JsonlContextTracerOptions): ContextTracer;
665
724
  /**
@@ -710,6 +769,20 @@ export declare class Agent<TContext = unknown> {
710
769
  private initMessage;
711
770
  private resultMessage;
712
771
  private modelTools;
772
+ /**
773
+ * Replaces the summarizable head of the conversation with a model-written
774
+ * summary. Returns undefined when there is nothing safe to compact, and lets a
775
+ * failed summarization surface so the caller can decide: compaction is best
776
+ * effort, and continuing with a full history is better than losing it.
777
+ */
778
+ private compactHistory;
779
+ /**
780
+ * Last-resort compaction after a turn already ran out of context, as opposed
781
+ * to the threshold check that runs between turns. Returns the message to emit
782
+ * so the caller can retry, or undefined when compaction cannot help and the
783
+ * original failure should surface instead.
784
+ */
785
+ private recoverFromOverflow;
713
786
  private messagesForModel;
714
787
  private selectSkills;
715
788
  private runTool;