klaus-agent 0.4.0 → 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.
- package/dist/approval/approval.d.ts +8 -3
- package/dist/approval/approval.js +23 -23
- package/dist/approval/approval.js.map +1 -1
- package/dist/approval/types.d.ts +1 -0
- package/dist/background/task-manager.d.ts +2 -0
- package/dist/background/task-manager.js +14 -0
- package/dist/background/task-manager.js.map +1 -1
- package/dist/compaction/compaction.d.ts +0 -1
- package/dist/compaction/compaction.js +21 -35
- package/dist/compaction/compaction.js.map +1 -1
- package/dist/compaction/summarizer.js +2 -7
- package/dist/compaction/summarizer.js.map +1 -1
- package/dist/core/agent-loop.d.ts +2 -1
- package/dist/core/agent-loop.js +14 -9
- package/dist/core/agent-loop.js.map +1 -1
- package/dist/core/agent.d.ts +1 -0
- package/dist/core/agent.js +11 -3
- package/dist/core/agent.js.map +1 -1
- package/dist/injection/history-normalizer.js +20 -10
- package/dist/injection/history-normalizer.js.map +1 -1
- package/dist/llm/types.d.ts +2 -0
- package/dist/multi-agent/task-executor.d.ts +2 -1
- package/dist/multi-agent/types.d.ts +3 -0
- package/dist/planning/planning-manager.d.ts +2 -0
- package/dist/planning/planning-manager.js +6 -0
- package/dist/planning/planning-manager.js.map +1 -1
- package/dist/providers/anthropic.js +14 -6
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/openai-codex.js +5 -8
- package/dist/providers/openai-codex.js.map +1 -1
- package/dist/providers/openai.js +3 -2
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/shared.d.ts +2 -2
- package/dist/providers/shared.js +11 -6
- package/dist/providers/shared.js.map +1 -1
- package/dist/session/session-manager.d.ts +1 -0
- package/dist/session/session-manager.js +11 -1
- package/dist/session/session-manager.js.map +1 -1
- package/dist/task-graph/task-graph.d.ts +3 -0
- package/dist/task-graph/task-graph.js +38 -13
- package/dist/task-graph/task-graph.js.map +1 -1
- package/dist/tools/executor.d.ts +3 -2
- package/dist/tools/mcp-adapter.js +22 -5
- package/dist/tools/mcp-adapter.js.map +1 -1
- package/dist/utils/id.js +2 -6
- package/dist/utils/id.js.map +1 -1
- package/dist/wire/wire.d.ts +2 -1
- package/package.json +1 -1
- package/src/approval/approval.ts +29 -23
- package/src/approval/types.ts +1 -0
- package/src/background/task-manager.ts +17 -0
- package/src/compaction/compaction.ts +23 -36
- package/src/compaction/summarizer.ts +2 -7
- package/src/core/agent-loop.ts +17 -10
- package/src/core/agent.ts +12 -3
- package/src/injection/history-normalizer.ts +22 -12
- package/src/llm/types.ts +2 -0
- package/src/multi-agent/task-executor.ts +1 -1
- package/src/multi-agent/types.ts +3 -0
- package/src/planning/planning-manager.ts +8 -0
- package/src/providers/anthropic.ts +70 -57
- package/src/providers/google.ts +1 -1
- package/src/providers/openai-codex.ts +7 -2
- package/src/providers/openai.ts +8 -3
- package/src/providers/shared.ts +11 -6
- package/src/session/session-manager.ts +15 -4
- package/src/task-graph/task-graph.ts +41 -13
- package/src/tools/executor.ts +2 -2
- package/src/tools/mcp-adapter.ts +23 -7
- package/src/utils/id.ts +3 -6
- package/src/wire/wire.ts +1 -1
|
@@ -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
|
|
27
|
-
? prevMsg.content
|
|
28
|
-
: prevMsg.content
|
|
29
|
-
const
|
|
30
|
-
? current.content
|
|
31
|
-
: current.content
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
7
|
+
interface TaskResult {
|
|
8
8
|
messages: AgentMessage[];
|
|
9
9
|
lastAssistantMessage?: AssistantMessage;
|
|
10
10
|
}
|
package/src/multi-agent/types.ts
CHANGED
|
@@ -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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
}
|
package/src/providers/google.ts
CHANGED
|
@@ -49,7 +49,7 @@ export class GeminiProvider implements LLMProvider {
|
|
|
49
49
|
maxOutputTokens: maxTokens ?? 8192,
|
|
50
50
|
...(thinkingBudget ? {
|
|
51
51
|
thinkingConfig: { thinkingBudget },
|
|
52
|
-
} as
|
|
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
|
-
|
|
330
|
-
try { reader.releaseLock(); } catch { /* ignore */ }
|
|
335
|
+
reader.cancel().catch(() => {});
|
|
331
336
|
}
|
|
332
337
|
}
|
|
333
338
|
|
package/src/providers/openai.ts
CHANGED
|
@@ -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
|
-
|
|
79
|
-
|
|
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
|
}
|
package/src/providers/shared.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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]
|
|
253
|
-
this.
|
|
254
|
-
this.
|
|
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
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Task graph — dependency-aware DAG with background execution and auto-unlock
|
|
2
2
|
|
|
3
|
-
import { readFileSync,
|
|
3
|
+
import { readFileSync, mkdirSync, existsSync } from "fs";
|
|
4
|
+
import { writeFile, rename } from "fs/promises";
|
|
4
5
|
import { join } from "path";
|
|
5
6
|
import { generateId } from "../utils/id.js";
|
|
6
7
|
import type { TaskNode, TaskStatus, TaskGraphConfig, CompletedTaskResult } from "./types.js";
|
|
@@ -22,11 +23,15 @@ export class TaskGraph {
|
|
|
22
23
|
|
|
23
24
|
get(id: string): TaskNode | undefined {
|
|
24
25
|
const task = this._tasks.get(id);
|
|
25
|
-
return task ? { ...task } : undefined;
|
|
26
|
+
return task ? { ...task, blockedBy: [...task.blockedBy], blocks: [...task.blocks] } : undefined;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
listAll(): TaskNode[] {
|
|
29
|
-
return [...this._tasks.values()]
|
|
30
|
+
return [...this._tasks.values()].map((t) => ({
|
|
31
|
+
...t,
|
|
32
|
+
blockedBy: [...t.blockedBy],
|
|
33
|
+
blocks: [...t.blocks],
|
|
34
|
+
}));
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
/** Tasks that are pending with no unfinished blockers. */
|
|
@@ -63,7 +68,7 @@ export class TaskGraph {
|
|
|
63
68
|
};
|
|
64
69
|
this._tasks.set(node.id, node);
|
|
65
70
|
this._persist();
|
|
66
|
-
return { ...node };
|
|
71
|
+
return { ...node, blockedBy: [...node.blockedBy], blocks: [...node.blocks] };
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
/** Add a dependency: `taskId` is blocked by `blockedById`. */
|
|
@@ -115,7 +120,7 @@ export class TaskGraph {
|
|
|
115
120
|
}
|
|
116
121
|
task.updatedAt = Date.now();
|
|
117
122
|
this._persist();
|
|
118
|
-
return { ...task };
|
|
123
|
+
return { ...task, blockedBy: [...task.blockedBy], blocks: [...task.blocks] };
|
|
119
124
|
}
|
|
120
125
|
|
|
121
126
|
|
|
@@ -242,13 +247,20 @@ export class TaskGraph {
|
|
|
242
247
|
return false;
|
|
243
248
|
}
|
|
244
249
|
|
|
250
|
+
private _persistPromise: Promise<void> = Promise.resolve();
|
|
251
|
+
|
|
245
252
|
private _persist(): void {
|
|
246
253
|
if (!this._config.persistDir) return;
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
254
|
+
const dir = this._config.persistDir;
|
|
255
|
+
const tasks = [...this._tasks.values()];
|
|
256
|
+
this._persistPromise = this._persistPromise
|
|
257
|
+
.then(() => {
|
|
258
|
+
const data = JSON.stringify(tasks, null, 2);
|
|
259
|
+
const target = join(dir, "tasks.json");
|
|
260
|
+
const tmp = target + ".tmp";
|
|
261
|
+
return writeFile(tmp, data, "utf-8").then(() => rename(tmp, target));
|
|
262
|
+
})
|
|
263
|
+
.catch(() => { /* persist failure is non-fatal */ });
|
|
252
264
|
}
|
|
253
265
|
|
|
254
266
|
private _loadFromDisk(): void {
|
|
@@ -259,12 +271,28 @@ export class TaskGraph {
|
|
|
259
271
|
const raw = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
260
272
|
if (!Array.isArray(raw)) return;
|
|
261
273
|
for (const entry of raw) {
|
|
262
|
-
if (
|
|
263
|
-
|
|
264
|
-
}
|
|
274
|
+
if (!this._isValidTaskNode(entry)) continue;
|
|
275
|
+
this._tasks.set(entry.id, entry as TaskNode);
|
|
265
276
|
}
|
|
266
277
|
} catch {
|
|
267
278
|
// Corrupted file — start fresh
|
|
268
279
|
}
|
|
269
280
|
}
|
|
281
|
+
|
|
282
|
+
private static readonly _validStatuses = new Set(["pending", "in_progress", "completed", "failed"]);
|
|
283
|
+
|
|
284
|
+
private _isValidTaskNode(entry: unknown): entry is TaskNode {
|
|
285
|
+
if (!entry || typeof entry !== "object") return false;
|
|
286
|
+
const e = entry as Record<string, unknown>;
|
|
287
|
+
return (
|
|
288
|
+
typeof e.id === "string" &&
|
|
289
|
+
typeof e.subject === "string" &&
|
|
290
|
+
typeof e.status === "string" &&
|
|
291
|
+
TaskGraph._validStatuses.has(e.status as string) &&
|
|
292
|
+
Array.isArray(e.blockedBy) &&
|
|
293
|
+
Array.isArray(e.blocks) &&
|
|
294
|
+
typeof e.createdAt === "number" &&
|
|
295
|
+
typeof e.updatedAt === "number"
|
|
296
|
+
);
|
|
297
|
+
}
|
|
270
298
|
}
|
package/src/tools/executor.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { ToolCallBlock, TextContent } from "../llm/types.js";
|
|
|
5
5
|
import type { Approval } from "../approval/types.js";
|
|
6
6
|
import { Value } from "@sinclair/typebox/value";
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
interface ToolExecutorConfig {
|
|
9
9
|
tools: AgentTool[];
|
|
10
10
|
mode: "sequential" | "parallel";
|
|
11
11
|
approval: Approval;
|
|
@@ -16,7 +16,7 @@ export interface ToolExecutorConfig {
|
|
|
16
16
|
onEvent: (event: ToolExecutorEvent) => void;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
type ToolExecutorEvent =
|
|
20
20
|
| { type: "tool_execution_start"; toolCallId: string; toolName: string; args: unknown }
|
|
21
21
|
| { type: "tool_execution_update"; toolCallId: string; toolName: string; partialResult: AgentToolResult }
|
|
22
22
|
| { type: "tool_execution_end"; toolCallId: string; toolName: string; result: AgentToolResult; isError: boolean };
|
package/src/tools/mcp-adapter.ts
CHANGED
|
@@ -156,17 +156,33 @@ export class MCPAdapter {
|
|
|
156
156
|
// Approval is handled by the executor via approvalAction field
|
|
157
157
|
// No need to request approval here
|
|
158
158
|
|
|
159
|
-
// Call with timeout
|
|
159
|
+
// Call with timeout and abort signal support
|
|
160
160
|
const callPromise = client.callTool(def.name, params);
|
|
161
161
|
let result: MCPToolResult;
|
|
162
162
|
|
|
163
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
164
|
+
let abortHandler: (() => void) | undefined;
|
|
165
|
+
const racers: Promise<MCPToolResult>[] = [callPromise];
|
|
166
|
+
|
|
163
167
|
if (timeout) {
|
|
164
|
-
|
|
165
|
-
setTimeout(() => reject(new Error(`MCP tool ${def.name} timed out after ${timeout}ms`)), timeout)
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
racers.push(new Promise<never>((_, reject) => {
|
|
169
|
+
timer = setTimeout(() => reject(new Error(`MCP tool ${def.name} timed out after ${timeout}ms`)), timeout);
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (context.signal.aborted) throw new Error("Aborted");
|
|
174
|
+
racers.push(new Promise<never>((_, reject) => {
|
|
175
|
+
abortHandler = () => reject(new Error("Aborted"));
|
|
176
|
+
context.signal.addEventListener("abort", abortHandler, { once: true });
|
|
177
|
+
}));
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
result = await Promise.race(racers);
|
|
181
|
+
} finally {
|
|
182
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
183
|
+
if (abortHandler) {
|
|
184
|
+
context.signal.removeEventListener("abort", abortHandler);
|
|
185
|
+
}
|
|
170
186
|
}
|
|
171
187
|
|
|
172
188
|
// Convert MCP result to AgentToolResult
|
package/src/utils/id.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
|
|
1
3
|
export function generateId(): string {
|
|
2
|
-
|
|
3
|
-
let id = "";
|
|
4
|
-
for (let i = 0; i < 12; i++) {
|
|
5
|
-
id += chars[Math.floor(Math.random() * chars.length)];
|
|
6
|
-
}
|
|
7
|
-
return id;
|
|
4
|
+
return randomBytes(9).toString("base64url").slice(0, 12);
|
|
8
5
|
}
|
package/src/wire/wire.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { generateId } from "../utils/id.js";
|
|
4
4
|
import type { WireMessage, WireSubscriber, WireSubscription } from "./types.js";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
interface WireOptions {
|
|
7
7
|
/** Max messages to buffer for replay to late subscribers. 0 = no buffering. */
|
|
8
8
|
bufferSize?: number;
|
|
9
9
|
}
|