klaus-agent 0.3.1 → 0.4.1

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.
Files changed (90) hide show
  1. package/README.md +36 -2
  2. package/README.zh-CN.md +35 -1
  3. package/dist/approval/approval.d.ts +8 -3
  4. package/dist/approval/approval.js +23 -23
  5. package/dist/approval/approval.js.map +1 -1
  6. package/dist/approval/types.d.ts +1 -0
  7. package/dist/background/task-manager.d.ts +2 -0
  8. package/dist/background/task-manager.js +14 -0
  9. package/dist/background/task-manager.js.map +1 -1
  10. package/dist/compaction/compaction.d.ts +0 -1
  11. package/dist/compaction/compaction.js +21 -35
  12. package/dist/compaction/compaction.js.map +1 -1
  13. package/dist/compaction/summarizer.js +2 -7
  14. package/dist/compaction/summarizer.js.map +1 -1
  15. package/dist/core/agent-loop.d.ts +2 -1
  16. package/dist/core/agent-loop.js +14 -9
  17. package/dist/core/agent-loop.js.map +1 -1
  18. package/dist/core/agent.d.ts +7 -0
  19. package/dist/core/agent.js +35 -10
  20. package/dist/core/agent.js.map +1 -1
  21. package/dist/index.d.ts +7 -0
  22. package/dist/index.js +5 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/injection/history-normalizer.js +20 -10
  25. package/dist/injection/history-normalizer.js.map +1 -1
  26. package/dist/llm/types.d.ts +2 -0
  27. package/dist/multi-agent/task-executor.d.ts +2 -1
  28. package/dist/multi-agent/types.d.ts +3 -0
  29. package/dist/planning/planning-manager.d.ts +2 -0
  30. package/dist/planning/planning-manager.js +6 -0
  31. package/dist/planning/planning-manager.js.map +1 -1
  32. package/dist/providers/anthropic.js +14 -6
  33. package/dist/providers/anthropic.js.map +1 -1
  34. package/dist/providers/google.js.map +1 -1
  35. package/dist/providers/openai-codex.js +5 -8
  36. package/dist/providers/openai-codex.js.map +1 -1
  37. package/dist/providers/openai.js +3 -2
  38. package/dist/providers/openai.js.map +1 -1
  39. package/dist/providers/shared.d.ts +2 -2
  40. package/dist/providers/shared.js +11 -6
  41. package/dist/providers/shared.js.map +1 -1
  42. package/dist/session/session-manager.d.ts +1 -0
  43. package/dist/session/session-manager.js +11 -1
  44. package/dist/session/session-manager.js.map +1 -1
  45. package/dist/task-graph/result-injection.d.ts +8 -0
  46. package/dist/task-graph/result-injection.js +26 -0
  47. package/dist/task-graph/result-injection.js.map +1 -0
  48. package/dist/task-graph/task-graph.d.ts +41 -0
  49. package/dist/task-graph/task-graph.js +266 -0
  50. package/dist/task-graph/task-graph.js.map +1 -0
  51. package/dist/task-graph/tools.d.ts +3 -0
  52. package/dist/task-graph/tools.js +106 -0
  53. package/dist/task-graph/tools.js.map +1 -0
  54. package/dist/task-graph/types.d.ts +44 -0
  55. package/dist/task-graph/types.js +9 -0
  56. package/dist/task-graph/types.js.map +1 -0
  57. package/dist/tools/executor.d.ts +3 -2
  58. package/dist/tools/mcp-adapter.js +22 -5
  59. package/dist/tools/mcp-adapter.js.map +1 -1
  60. package/dist/utils/id.js +2 -6
  61. package/dist/utils/id.js.map +1 -1
  62. package/dist/wire/wire.d.ts +2 -1
  63. package/package.json +1 -1
  64. package/src/approval/approval.ts +29 -23
  65. package/src/approval/types.ts +1 -0
  66. package/src/background/task-manager.ts +17 -0
  67. package/src/compaction/compaction.ts +23 -36
  68. package/src/compaction/summarizer.ts +2 -7
  69. package/src/core/agent-loop.ts +17 -10
  70. package/src/core/agent.ts +41 -9
  71. package/src/index.ts +15 -0
  72. package/src/injection/history-normalizer.ts +22 -12
  73. package/src/llm/types.ts +2 -0
  74. package/src/multi-agent/task-executor.ts +1 -1
  75. package/src/multi-agent/types.ts +3 -0
  76. package/src/planning/planning-manager.ts +8 -0
  77. package/src/providers/anthropic.ts +70 -57
  78. package/src/providers/google.ts +1 -1
  79. package/src/providers/openai-codex.ts +7 -2
  80. package/src/providers/openai.ts +8 -3
  81. package/src/providers/shared.ts +11 -6
  82. package/src/session/session-manager.ts +15 -4
  83. package/src/task-graph/result-injection.ts +29 -0
  84. package/src/task-graph/task-graph.ts +298 -0
  85. package/src/task-graph/tools.ts +109 -0
  86. package/src/task-graph/types.ts +52 -0
  87. package/src/tools/executor.ts +2 -2
  88. package/src/tools/mcp-adapter.ts +23 -7
  89. package/src/utils/id.ts +3 -6
  90. package/src/wire/wire.ts +1 -1
package/src/core/agent.ts CHANGED
@@ -22,6 +22,7 @@ import type { SkillSource } from "../skills/types.js";
22
22
  import type { MCPServerConfig, MCPClient } from "../tools/mcp-adapter.js";
23
23
  import type { TaskFactory } from "../background/types.js";
24
24
  import type { PlanningConfig } from "../planning/types.js";
25
+ import type { TaskGraphConfig } from "../task-graph/types.js";
25
26
  import { SessionManager } from "../session/session-manager.js";
26
27
  import { CheckpointManager } from "../checkpoint/checkpoint-manager.js";
27
28
  import { InjectionManager } from "../injection/injection-manager.js";
@@ -39,6 +40,10 @@ import { createBackgroundTaskTools } from "../background/tools.js";
39
40
  import { PlanningManager } from "../planning/planning-manager.js";
40
41
  import { createPlanningTools } from "../planning/tools.js";
41
42
  import { PlanningNagProvider } from "../planning/nag-injection.js";
43
+ import { TaskGraph } from "../task-graph/task-graph.js";
44
+ import { createTaskGraphTools } from "../task-graph/tools.js";
45
+ import { TaskResultInjectionProvider } from "../task-graph/result-injection.js";
46
+ import { resolveProvider } from "../llm/provider.js";
42
47
  import { runAgentLoop } from "./agent-loop.js";
43
48
 
44
49
  export interface AgentConfig {
@@ -64,6 +69,7 @@ export interface AgentConfig {
64
69
  mcp?: { servers: MCPServerConfig[]; clientFactory: (config: MCPServerConfig) => MCPClient };
65
70
  wire?: { bufferSize?: number };
66
71
  backgroundTasks?: { factories?: Record<string, TaskFactory> };
72
+ taskGraph?: TaskGraphConfig;
67
73
  planning?: PlanningConfig;
68
74
  }
69
75
 
@@ -89,6 +95,8 @@ export class Agent {
89
95
  private _wire: Wire;
90
96
  private _backgroundTaskManager: BackgroundTaskManager | undefined;
91
97
  private _planningManager: PlanningManager | undefined;
98
+ private _taskGraph: TaskGraph;
99
+ private _createdSubagents: Agent[] = [];
92
100
  private _initialized = false;
93
101
 
94
102
  constructor(config: AgentConfig) {
@@ -156,6 +164,9 @@ export class Agent {
156
164
  if (config.planning) {
157
165
  this._planningManager = new PlanningManager(config.planning);
158
166
  }
167
+
168
+ // Task graph
169
+ this._taskGraph = new TaskGraph(config.taskGraph ?? {});
159
170
  }
160
171
 
161
172
  // --- Public API ---
@@ -253,6 +264,10 @@ export class Agent {
253
264
  return this._planningManager;
254
265
  }
255
266
 
267
+ get taskGraph(): TaskGraph {
268
+ return this._taskGraph;
269
+ }
270
+
256
271
  setSystemPrompt(prompt: string): void {
257
272
  this._state.systemPrompt = prompt;
258
273
  }
@@ -280,8 +295,12 @@ export class Agent {
280
295
  this._listeners.clear();
281
296
  this._steeringQueue = [];
282
297
  this._followUpQueue = [];
298
+ await Promise.allSettled(this._createdSubagents.map((sub) => sub.dispose()));
299
+ this._createdSubagents = [];
300
+ this._approval.dispose();
283
301
  await this._mcpAdapter?.dispose();
284
302
  this._backgroundTaskManager?.dispose();
303
+ this._taskGraph.dispose();
285
304
  this._wire.dispose();
286
305
  }
287
306
 
@@ -289,7 +308,6 @@ export class Agent {
289
308
 
290
309
  private async _ensureInitialized(): Promise<void> {
291
310
  if (this._initialized) return;
292
- this._initialized = true;
293
311
 
294
312
  // Init session and restore messages
295
313
  if (this._sessionManager) {
@@ -348,14 +366,17 @@ export class Agent {
348
366
  // Init fixed subagents and create TaskTool
349
367
  if (this._config.subagents && this._laborMarket && this._taskExecutor) {
350
368
  for (const [name, subConfig] of Object.entries(this._config.subagents)) {
369
+ const subModel = subConfig.model ?? this._config.model;
370
+ const subProvider = subConfig.model ? resolveProvider(subConfig.model) : this._provider;
351
371
  const subAgent = new Agent({
352
- model: this._config.model,
372
+ model: subModel,
353
373
  systemPrompt: subConfig.systemPrompt,
354
374
  tools: subConfig.tools ?? [],
355
- provider: this._provider,
375
+ provider: subProvider,
356
376
  approval: this._approval.share(),
357
377
  name,
358
378
  });
379
+ this._createdSubagents.push(subAgent);
359
380
  this._laborMarket.addFixed(name, subAgent, subConfig.description);
360
381
  }
361
382
  // Add built-in TaskTool so LLM can delegate to subagents
@@ -373,12 +394,23 @@ export class Agent {
373
394
  this._state.tools = [...this._state.tools, ...createPlanningTools(this._planningManager)];
374
395
 
375
396
  // Register nag provider into injection manager (create one if needed)
376
- const nagProvider = new PlanningNagProvider(this._planningManager);
377
- if (this._injectionManager) {
378
- this._injectionManager.addProvider(nagProvider);
379
- } else {
380
- this._injectionManager = new InjectionManager([nagProvider]);
381
- }
397
+ this._addInjectionProvider(new PlanningNagProvider(this._planningManager));
398
+ }
399
+
400
+ // Task graph tools + result auto-injection
401
+ this._state.tools = [...this._state.tools, ...createTaskGraphTools(this._taskGraph)];
402
+ if (this._config.taskGraph?.autoInjectResults !== false) {
403
+ this._addInjectionProvider(new TaskResultInjectionProvider(this._taskGraph));
404
+ }
405
+
406
+ this._initialized = true;
407
+ }
408
+
409
+ private _addInjectionProvider(provider: DynamicInjectionProvider): void {
410
+ if (this._injectionManager) {
411
+ this._injectionManager.addProvider(provider);
412
+ } else {
413
+ this._injectionManager = new InjectionManager([provider]);
382
414
  }
383
415
  }
384
416
 
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@ import type { SkillSource } from "./skills/types.js";
16
16
  import type { MCPServerConfig, MCPClient } from "./tools/mcp-adapter.js";
17
17
  import type { TaskFactory } from "./background/types.js";
18
18
  import type { PlanningConfig } from "./planning/types.js";
19
+ import type { TaskGraphConfig } from "./task-graph/types.js";
19
20
 
20
21
  export interface CreateAgentConfig {
21
22
  // Required
@@ -43,6 +44,7 @@ export interface CreateAgentConfig {
43
44
  wire?: { bufferSize?: number };
44
45
  backgroundTasks?: { factories?: Record<string, TaskFactory> };
45
46
  planning?: PlanningConfig;
47
+ taskGraph?: TaskGraphConfig;
46
48
 
47
49
  // Advanced: provide your own LLM provider
48
50
  provider?: LLMProvider;
@@ -74,6 +76,7 @@ export function createAgent(config: CreateAgentConfig): Agent {
74
76
  wire: config.wire,
75
77
  backgroundTasks: config.backgroundTasks,
76
78
  planning: config.planning,
79
+ taskGraph: config.taskGraph,
77
80
  });
78
81
  }
79
82
 
@@ -107,6 +110,9 @@ export { createBackgroundTaskTools } from "./background/tools.js";
107
110
  export { PlanningManager } from "./planning/planning-manager.js";
108
111
  export { createPlanningTools } from "./planning/tools.js";
109
112
  export { PlanningNagProvider } from "./planning/nag-injection.js";
113
+ export { TaskGraph } from "./task-graph/task-graph.js";
114
+ export { createTaskGraphTools } from "./task-graph/tools.js";
115
+ export { TaskResultInjectionProvider } from "./task-graph/result-injection.js";
110
116
 
111
117
  // Core types
112
118
  export type {
@@ -243,3 +249,12 @@ export type {
243
249
  } from "./planning/types.js";
244
250
 
245
251
  export { PLANNING_TOOL_NAMES } from "./planning/types.js";
252
+ export { TASK_GRAPH_TOOL_NAMES } from "./task-graph/types.js";
253
+
254
+ // Task graph types
255
+ export type {
256
+ TaskGraphConfig,
257
+ TaskNode,
258
+ TaskStatus,
259
+ CompletedTaskResult,
260
+ } from "./task-graph/types.js";
@@ -1,6 +1,6 @@
1
1
  // History normalizer — merge adjacent user messages
2
2
 
3
- import type { AgentMessage, Message } from "../types.js";
3
+ import type { AgentMessage, Message, ContentBlock } from "../types.js";
4
4
 
5
5
  export function normalizeHistory(messages: AgentMessage[]): AgentMessage[] {
6
6
  if (messages.length <= 1) return messages;
@@ -23,17 +23,27 @@ export function normalizeHistory(messages: AgentMessage[]): AgentMessage[] {
23
23
  (prev as Message).role === "user"
24
24
  ) {
25
25
  const prevMsg = prev as Message & { role: "user" };
26
- const prevText = typeof prevMsg.content === "string"
27
- ? prevMsg.content
28
- : prevMsg.content.filter((b) => b.type === "text").map((b) => (b as { text: string }).text).join("\n");
29
- const currentText = typeof current.content === "string"
30
- ? current.content
31
- : current.content.filter((b) => b.type === "text").map((b) => (b as { text: string }).text).join("\n");
32
-
33
- result[result.length - 1] = {
34
- role: "user",
35
- content: prevText + "\n" + currentText,
36
- };
26
+ const prevBlocks = typeof prevMsg.content === "string"
27
+ ? [{ type: "text" as const, text: prevMsg.content }]
28
+ : prevMsg.content;
29
+ const currentBlocks = typeof current.content === "string"
30
+ ? [{ type: "text" as const, text: current.content }]
31
+ : current.content;
32
+
33
+ const merged: ContentBlock[] = [...prevBlocks, ...currentBlocks];
34
+
35
+ // Optimize: if all blocks are text, collapse to a single string
36
+ if (merged.every((b) => b.type === "text")) {
37
+ result[result.length - 1] = {
38
+ role: "user",
39
+ content: merged.map((b) => (b as { text: string }).text).join("\n"),
40
+ };
41
+ } else {
42
+ result[result.length - 1] = {
43
+ role: "user",
44
+ content: merged,
45
+ };
46
+ }
37
47
  continue;
38
48
  }
39
49
 
package/src/llm/types.ts CHANGED
@@ -76,6 +76,8 @@ export interface TextBlock {
76
76
  export interface ThinkingBlock {
77
77
  type: "thinking";
78
78
  thinking: string;
79
+ /** Opaque signature returned by the provider; must be echoed back in subsequent requests. */
80
+ signature?: string;
79
81
  }
80
82
 
81
83
  export type AssistantContentBlock = TextBlock | ToolCallBlock | ThinkingBlock;
@@ -4,7 +4,7 @@ import type { Agent } from "../core/agent.js";
4
4
  import type { LaborMarket } from "./labor-market.js";
5
5
  import type { AgentMessage, AgentEvent, AssistantMessage } from "../types.js";
6
6
 
7
- export interface TaskResult {
7
+ interface TaskResult {
8
8
  messages: AgentMessage[];
9
9
  lastAssistantMessage?: AssistantMessage;
10
10
  }
@@ -1,10 +1,13 @@
1
1
  // Multi-agent types
2
2
 
3
3
  import type { AgentTool } from "../tools/types.js";
4
+ import type { ModelConfig } from "../llm/types.js";
4
5
 
5
6
  export interface SubagentConfig {
6
7
  name: string;
7
8
  systemPrompt: string | (() => string | Promise<string>);
8
9
  tools?: AgentTool[];
9
10
  description: string;
11
+ /** Override the parent agent's model config. If omitted, inherits the parent's model. */
12
+ model?: ModelConfig;
10
13
  }
@@ -4,6 +4,9 @@ import type { TodoItem, TodoStatus, PlanPhase, PlanningState, PlanningConfig } f
4
4
  import { PLANNING_TOOL_NAMES } from "./types.js";
5
5
  import { generateId } from "../utils/id.js";
6
6
 
7
+ /** Number of built-in tools always added to allowedInPlanning (todo + plan_mode). */
8
+ const BUILT_IN_PLANNING_TOOL_COUNT = Object.keys(PLANNING_TOOL_NAMES).length;
9
+
7
10
  export class PlanningManager {
8
11
  private _state: PlanningState;
9
12
  private _config: PlanningConfig;
@@ -43,6 +46,11 @@ export class PlanningManager {
43
46
  return this._allowedInPlanning;
44
47
  }
45
48
 
49
+ /** Whether user configured read-only tools beyond the built-in planning tools. */
50
+ get hasConfiguredReadOnlyTools(): boolean {
51
+ return this._allowedInPlanning.size > BUILT_IN_PLANNING_TOOL_COUNT;
52
+ }
53
+
46
54
  // --- Phase control ---
47
55
 
48
56
  startExecution(): string {
@@ -11,6 +11,12 @@ import type {
11
11
  } from "../llm/types.js";
12
12
  import { withRetry, RETRYABLE_PATTERNS, mapThinkingBudget } from "./shared.js";
13
13
 
14
+ // Anthropic SDK type extension not yet in published typings
15
+ interface ContentBlockDeltaSignature {
16
+ type: "signature_delta";
17
+ signature: string;
18
+ }
19
+
14
20
  export class AnthropicProvider implements LLMProvider {
15
21
  private client: Anthropic;
16
22
 
@@ -72,70 +78,77 @@ export class AnthropicProvider implements LLMProvider {
72
78
  const stream = this.client.messages.stream(params, { signal });
73
79
 
74
80
  for await (const event of stream) {
75
- if (event.type === "content_block_start") {
76
- const block = event.content_block;
77
- if (block.type === "text") {
78
- contentBlocks.push({ type: "text", text: "" });
79
- } else if (block.type === "tool_use") {
80
- contentBlocks.push({ type: "tool_call", id: block.id, name: block.name, input: {} });
81
- toolInputBuffers.set(event.index, "");
82
- yield { type: "tool_call_start", id: block.id, name: block.name };
83
- } else if (block.type === "thinking") {
84
- contentBlocks.push({ type: "thinking", thinking: "" });
85
- }
86
- } else if (event.type === "content_block_delta") {
87
- const delta = event.delta;
88
- if (delta.type === "text_delta") {
89
- const block = contentBlocks[event.index];
90
- if (block && block.type === "text") {
91
- block.text += delta.text;
92
- }
93
- yield { type: "text", text: delta.text };
94
- } else if (delta.type === "input_json_delta") {
95
- const buf = (toolInputBuffers.get(event.index) ?? "") + delta.partial_json;
96
- toolInputBuffers.set(event.index, buf);
97
- const block = contentBlocks[event.index];
98
- if (block && block.type === "tool_call") {
99
- yield { type: "tool_call_delta", id: block.id, input: delta.partial_json };
100
- }
101
- } else if (delta.type === "thinking_delta") {
102
- const block = contentBlocks[event.index];
103
- if (block && block.type === "thinking") {
104
- block.thinking += delta.thinking;
105
- }
106
- yield { type: "thinking", thinking: delta.thinking };
81
+ if (event.type === "content_block_start") {
82
+ const block = event.content_block;
83
+ if (block.type === "text") {
84
+ contentBlocks.push({ type: "text", text: "" });
85
+ } else if (block.type === "tool_use") {
86
+ contentBlocks.push({ type: "tool_call", id: block.id, name: block.name, input: {} });
87
+ toolInputBuffers.set(event.index, "");
88
+ yield { type: "tool_call_start", id: block.id, name: block.name };
89
+ } else if (block.type === "thinking") {
90
+ contentBlocks.push({ type: "thinking", thinking: "" });
91
+ }
92
+ } else if (event.type === "content_block_delta") {
93
+ const delta = event.delta;
94
+ if (delta.type === "text_delta") {
95
+ const block = contentBlocks[event.index];
96
+ if (block && block.type === "text") {
97
+ block.text += delta.text;
107
98
  }
108
- } else if (event.type === "content_block_stop") {
99
+ yield { type: "text", text: delta.text };
100
+ } else if (delta.type === "input_json_delta") {
101
+ const buf = (toolInputBuffers.get(event.index) ?? "") + delta.partial_json;
102
+ toolInputBuffers.set(event.index, buf);
109
103
  const block = contentBlocks[event.index];
110
104
  if (block && block.type === "tool_call") {
111
- const buf = toolInputBuffers.get(event.index) ?? "{}";
112
- try {
113
- block.input = JSON.parse(buf || "{}");
114
- } catch {
115
- block.input = {};
116
- }
117
- toolInputBuffers.delete(event.index);
105
+ yield { type: "tool_call_delta", id: block.id, input: delta.partial_json };
106
+ }
107
+ } else if (delta.type === "thinking_delta") {
108
+ const block = contentBlocks[event.index];
109
+ if (block && block.type === "thinking") {
110
+ block.thinking += delta.thinking;
118
111
  }
119
- } else if (event.type === "message_delta") {
120
- if (event.usage) {
121
- usage = {
122
- inputTokens: usage.inputTokens,
123
- outputTokens: event.usage.output_tokens,
124
- totalTokens: usage.inputTokens + event.usage.output_tokens,
125
- };
112
+ yield { type: "thinking", thinking: delta.thinking };
113
+ } else if ((delta as unknown as ContentBlockDeltaSignature).type === "signature_delta") {
114
+ const sigDelta = delta as unknown as ContentBlockDeltaSignature;
115
+ const block = contentBlocks[event.index];
116
+ if (block && block.type === "thinking") {
117
+ block.signature = (block.signature ?? "") + sigDelta.signature;
126
118
  }
127
- } else if (event.type === "message_start") {
128
- if (event.message.usage) {
129
- usage = {
130
- inputTokens: event.message.usage.input_tokens,
131
- outputTokens: event.message.usage.output_tokens,
132
- totalTokens: event.message.usage.input_tokens + event.message.usage.output_tokens,
133
- cacheReadTokens: (event.message.usage as any).cache_read_input_tokens,
134
- cacheWriteTokens: (event.message.usage as any).cache_creation_input_tokens,
135
- };
119
+ }
120
+ } else if (event.type === "content_block_stop") {
121
+ const block = contentBlocks[event.index];
122
+ if (block && block.type === "tool_call") {
123
+ const buf = toolInputBuffers.get(event.index) ?? "{}";
124
+ try {
125
+ block.input = JSON.parse(buf || "{}");
126
+ } catch {
127
+ block.input = {};
136
128
  }
129
+ toolInputBuffers.delete(event.index);
130
+ }
131
+ } else if (event.type === "message_delta") {
132
+ if (event.usage) {
133
+ usage = {
134
+ inputTokens: usage.inputTokens,
135
+ outputTokens: event.usage.output_tokens,
136
+ totalTokens: usage.inputTokens + event.usage.output_tokens,
137
+ };
138
+ }
139
+ } else if (event.type === "message_start") {
140
+ if (event.message.usage) {
141
+ const u = event.message.usage;
142
+ usage = {
143
+ inputTokens: u.input_tokens,
144
+ outputTokens: u.output_tokens,
145
+ totalTokens: u.input_tokens + u.output_tokens,
146
+ cacheReadTokens: u.cache_read_input_tokens ?? undefined,
147
+ cacheWriteTokens: u.cache_creation_input_tokens ?? undefined,
148
+ };
137
149
  }
138
150
  }
151
+ }
139
152
 
140
153
  const message: AssistantMessage = { role: "assistant", content: contentBlocks };
141
154
  yield { type: "done", message, usage };
@@ -179,7 +192,7 @@ function mapAssistantBlock(block: AssistantContentBlock): Anthropic.ContentBlock
179
192
  return { type: "tool_use", id: block.id, name: block.name, input: block.input };
180
193
  }
181
194
  if (block.type === "thinking") {
182
- return { type: "thinking", thinking: block.thinking, signature: "" };
195
+ return { type: "thinking", thinking: block.thinking, signature: block.signature ?? "" };
183
196
  }
184
197
  return { type: "text", text: JSON.stringify(block) };
185
198
  }
@@ -49,7 +49,7 @@ export class GeminiProvider implements LLMProvider {
49
49
  maxOutputTokens: maxTokens ?? 8192,
50
50
  ...(thinkingBudget ? {
51
51
  thinkingConfig: { thinkingBudget },
52
- } as any : {}),
52
+ } as Record<string, unknown> : {}),
53
53
  },
54
54
  },
55
55
  this.baseUrl ? { baseUrl: this.baseUrl } : undefined,
@@ -291,6 +291,8 @@ function mapReasoningEffort(modelId: string, level?: ThinkingLevel): string | un
291
291
 
292
292
  // --- SSE parsing ---
293
293
 
294
+ const MAX_SSE_BUFFER_BYTES = 1024 * 1024; // 1MB cap to prevent unbounded accumulation
295
+
294
296
  async function* parseSSE(response: Response): AsyncGenerator<Record<string, unknown>> {
295
297
  if (!response.body) return;
296
298
 
@@ -304,6 +306,10 @@ async function* parseSSE(response: Response): AsyncGenerator<Record<string, unkn
304
306
  if (done) break;
305
307
  buffer += decoder.decode(value, { stream: true });
306
308
 
309
+ if (buffer.length > MAX_SSE_BUFFER_BYTES) {
310
+ throw new Error(`SSE buffer exceeded ${MAX_SSE_BUFFER_BYTES} bytes — aborting to prevent unbounded memory growth`);
311
+ }
312
+
307
313
  let idx = buffer.indexOf("\n\n");
308
314
  while (idx !== -1) {
309
315
  const chunk = buffer.slice(0, idx);
@@ -326,8 +332,7 @@ async function* parseSSE(response: Response): AsyncGenerator<Record<string, unkn
326
332
  }
327
333
  }
328
334
  } finally {
329
- try { await reader.cancel(); } catch { /* ignore */ }
330
- try { reader.releaseLock(); } catch { /* ignore */ }
335
+ reader.cancel().catch(() => {});
331
336
  }
332
337
  }
333
338
 
@@ -8,11 +8,15 @@ import type {
8
8
  AssistantMessage,
9
9
  AssistantContentBlock,
10
10
  TokenUsage,
11
- ThinkingLevel,
12
11
  Message,
13
12
  } from "../llm/types.js";
14
13
  import { withRetry, RETRYABLE_PATTERNS, mapReasoningEffort } from "./shared.js";
15
14
 
15
+ // OpenAI o1/o3 reasoning content not yet in published typings
16
+ interface DeltaWithReasoning {
17
+ reasoning_content?: string;
18
+ }
19
+
16
20
  export class OpenAIProvider implements LLMProvider {
17
21
  private client: OpenAI;
18
22
 
@@ -75,8 +79,9 @@ export class OpenAIProvider implements LLMProvider {
75
79
  }
76
80
 
77
81
  // Reasoning/thinking content (o1/o3 series)
78
- if ((delta as any).reasoning_content) {
79
- const thinking = (delta as any).reasoning_content as string;
82
+ const reasoningContent = (delta as unknown as DeltaWithReasoning).reasoning_content;
83
+ if (reasoningContent) {
84
+ const thinking = reasoningContent;
80
85
  if (contentBlocks.length === 0 || contentBlocks[contentBlocks.length - 1].type !== "thinking") {
81
86
  contentBlocks.push({ type: "thinking", thinking: "" });
82
87
  }
@@ -12,13 +12,14 @@ export const RETRYABLE_PATTERNS: Record<string, string[]> = {
12
12
  codex: [...COMMON_RETRYABLE, "rate_limit", "usage_limit", "overloaded"],
13
13
  };
14
14
 
15
- export function isRetryableError(error: Error, patterns: string[]): boolean {
15
+ function isRetryableError(error: Error, patterns: string[]): boolean {
16
16
  return patterns.some((p) => error.message.includes(p));
17
17
  }
18
18
 
19
19
  /**
20
20
  * Wraps a streaming generator with exponential backoff retry logic.
21
- * Only retries on connection-level failures before streaming starts.
21
+ * Only retries on connection-level failures before any events have been yielded.
22
+ * Once streaming starts (first event yielded), errors are not retried to avoid duplicate output.
22
23
  */
23
24
  export async function* withRetry(
24
25
  streamOnce: () => AsyncIterable<AssistantMessageEvent>,
@@ -33,15 +34,19 @@ export async function* withRetry(
33
34
  await new Promise((r) => setTimeout(r, delay));
34
35
  }
35
36
 
37
+ let hasYielded = false;
36
38
  try {
37
- yield* streamOnce();
39
+ for await (const event of streamOnce()) {
40
+ hasYielded = true;
41
+ yield event;
42
+ }
38
43
  return;
39
44
  } catch (err) {
40
45
  lastError = err instanceof Error ? err : new Error(String(err));
41
46
 
42
- if (!isRetryableError(lastError, retryablePatterns) || attempt === maxRetries) {
43
- yield { type: "error", error: lastError };
44
- return;
47
+ // If we already yielded events, don't retry caller has consumed partial output
48
+ if (hasYielded || !isRetryableError(lastError, retryablePatterns) || attempt === maxRetries) {
49
+ throw lastError;
45
50
  }
46
51
  }
47
52
  }
@@ -247,12 +247,23 @@ export class SessionManager {
247
247
  this._sessionId = (header as SessionHeader).id;
248
248
  }
249
249
 
250
- // Rest are entries
250
+ // Rest are entries — validate required fields before accepting
251
251
  for (let i = 1; i < records.length; i++) {
252
- const entry = records[i] as SessionEntry;
253
- this._entries.push(entry);
254
- this._entriesById.set(entry.id, entry);
252
+ const entry = records[i];
253
+ if (!this._isValidEntry(entry)) continue;
254
+ this._entries.push(entry as SessionEntry);
255
+ this._entriesById.set(entry.id, entry as SessionEntry);
255
256
  this._leafId = entry.id;
256
257
  }
257
258
  }
259
+
260
+ private _isValidEntry(record: unknown): record is SessionEntry {
261
+ if (!record || typeof record !== "object") return false;
262
+ const r = record as Record<string, unknown>;
263
+ return (
264
+ typeof r.type === "string" &&
265
+ typeof r.id === "string" &&
266
+ typeof r.timestamp === "string"
267
+ );
268
+ }
258
269
  }
@@ -0,0 +1,29 @@
1
+ // Auto-inject completed background task results before each LLM call
2
+
3
+ import type { DynamicInjectionProvider, DynamicInjection } from "../injection/types.js";
4
+ import type { AgentMessage } from "../types.js";
5
+ import type { TaskGraph } from "./task-graph.js";
6
+
7
+ export class TaskResultInjectionProvider implements DynamicInjectionProvider {
8
+ constructor(private _graph: TaskGraph) {}
9
+
10
+ async getInjections(_history: AgentMessage[]): Promise<DynamicInjection[]> {
11
+ const completed = this._graph.drainCompleted();
12
+ if (completed.length === 0) return [];
13
+
14
+ const lines = completed.map((c) => {
15
+ const status = c.status === "completed" ? "completed" : "FAILED";
16
+ const unblocked = c.unblockedTasks.length > 0
17
+ ? ` → unblocked: ${c.unblockedTasks.join(", ")}`
18
+ : "";
19
+ return `[task:${c.taskId}] ${c.subject} — ${status}: ${c.result}${unblocked}`;
20
+ });
21
+
22
+ return [
23
+ {
24
+ type: "task-results",
25
+ content: `<background-results>\n${lines.join("\n")}\n</background-results>`,
26
+ },
27
+ ];
28
+ }
29
+ }