negotium 0.1.6 → 0.1.8
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 +2 -2
- package/dist/chunk-0ay6cjdy.js +1331 -0
- package/dist/chunk-0ay6cjdy.js.map +16 -0
- package/dist/chunk-1k7t7bnx.js +97 -0
- package/dist/chunk-1k7t7bnx.js.map +10 -0
- package/dist/chunk-5ahkew9c.js +806 -0
- package/dist/chunk-5ahkew9c.js.map +17 -0
- package/dist/chunk-6cpn7ht2.js +287 -0
- package/dist/chunk-6cpn7ht2.js.map +13 -0
- package/dist/chunk-6kctn20e.js +3529 -0
- package/dist/chunk-6kctn20e.js.map +20 -0
- package/dist/chunk-7xqj7aex.js +77 -0
- package/dist/chunk-7xqj7aex.js.map +11 -0
- package/dist/chunk-9p0ww3tk.js +41 -0
- package/dist/chunk-9p0ww3tk.js.map +10 -0
- package/dist/chunk-a92a7nrk.js +252 -0
- package/dist/chunk-a92a7nrk.js.map +10 -0
- package/dist/chunk-c500xe9z.js +33 -0
- package/dist/chunk-c500xe9z.js.map +10 -0
- package/dist/chunk-cds1qxsf.js +1644 -0
- package/dist/chunk-cds1qxsf.js.map +17 -0
- package/dist/chunk-e4407g8x.js +274 -0
- package/dist/chunk-e4407g8x.js.map +10 -0
- package/dist/chunk-e5pbggsh.js +363 -0
- package/dist/chunk-e5pbggsh.js.map +11 -0
- package/dist/chunk-edb07sjp.js +567 -0
- package/dist/chunk-edb07sjp.js.map +14 -0
- package/dist/chunk-exd0v74a.js +3012 -0
- package/dist/chunk-exd0v74a.js.map +32 -0
- package/dist/chunk-fgrme9tr.js +4398 -0
- package/dist/chunk-fgrme9tr.js.map +47 -0
- package/dist/chunk-fka9eqe1.js +581 -0
- package/dist/chunk-fka9eqe1.js.map +11 -0
- package/dist/chunk-g82jygez.js +108 -0
- package/dist/chunk-g82jygez.js.map +10 -0
- package/dist/chunk-ghs2j1fr.js +312 -0
- package/dist/chunk-ghs2j1fr.js.map +10 -0
- package/dist/chunk-gmy11efb.js +125 -0
- package/dist/chunk-gmy11efb.js.map +10 -0
- package/dist/chunk-gxma4c70.js +110 -0
- package/dist/chunk-gxma4c70.js.map +10 -0
- package/dist/chunk-hkdqhm34.js +61 -0
- package/dist/chunk-hkdqhm34.js.map +10 -0
- package/dist/chunk-hpa0cpzh.js +134 -0
- package/dist/chunk-hpa0cpzh.js.map +10 -0
- package/dist/chunk-hwy3n31w.js +21 -0
- package/dist/chunk-hwy3n31w.js.map +9 -0
- package/dist/chunk-j2behpy7.js +214 -0
- package/dist/chunk-j2behpy7.js.map +10 -0
- package/dist/chunk-jc0at0tc.js +102 -0
- package/dist/chunk-jc0at0tc.js.map +10 -0
- package/dist/chunk-jkqq5ef6.js +243 -0
- package/dist/chunk-jkqq5ef6.js.map +11 -0
- package/dist/chunk-jzkw2f01.js +51 -0
- package/dist/chunk-jzkw2f01.js.map +10 -0
- package/dist/chunk-jzz91jwh.js +70 -0
- package/dist/chunk-jzz91jwh.js.map +10 -0
- package/dist/chunk-k9z47mab.js +247 -0
- package/dist/chunk-k9z47mab.js.map +12 -0
- package/dist/chunk-n17yxpa8.js +23 -0
- package/dist/chunk-n17yxpa8.js.map +10 -0
- package/dist/chunk-naa09ags.js +2143 -0
- package/dist/chunk-naa09ags.js.map +15 -0
- package/dist/chunk-ns3ke88n.js +142 -0
- package/dist/chunk-ns3ke88n.js.map +10 -0
- package/dist/chunk-p3cr2ejg.js +1626 -0
- package/dist/chunk-p3cr2ejg.js.map +15 -0
- package/dist/chunk-q5bcpf23.js +2925 -0
- package/dist/chunk-q5bcpf23.js.map +20 -0
- package/dist/chunk-rafc55bs.js +424 -0
- package/dist/chunk-rafc55bs.js.map +11 -0
- package/dist/chunk-rbrxjvbj.js +2830 -0
- package/dist/chunk-rbrxjvbj.js.map +23 -0
- package/dist/chunk-sa874j5c.js +9 -0
- package/dist/chunk-sa874j5c.js.map +10 -0
- package/dist/chunk-sgsz2fpq.js +289 -0
- package/dist/chunk-sgsz2fpq.js.map +10 -0
- package/dist/chunk-sm6fh5c5.js +569 -0
- package/dist/chunk-sm6fh5c5.js.map +14 -0
- package/dist/chunk-t9zcg8f1.js +499 -0
- package/dist/chunk-t9zcg8f1.js.map +13 -0
- package/dist/chunk-vp2db42d.js +121 -0
- package/dist/chunk-vp2db42d.js.map +10 -0
- package/dist/chunk-w13jjf4a.js +46 -0
- package/dist/chunk-w13jjf4a.js.map +10 -0
- package/dist/chunk-x1arr7ze.js +63 -0
- package/dist/chunk-x1arr7ze.js.map +10 -0
- package/dist/chunk-xfq68rch.js +583 -0
- package/dist/chunk-xfq68rch.js.map +11 -0
- package/dist/main.js +132 -20
- package/dist/main.js.map +4 -4
- package/dist/runtime/scripts/faster-whisper-wrapper.py +87 -0
- package/dist/runtime/scripts/mcp-patchright-http.mjs +349 -0
- package/dist/runtime/src/agents/api-topic-agent-switch.ts +178 -0
- package/dist/runtime/src/agents/archiver.ts +396 -0
- package/dist/runtime/src/agents/auth-check.ts +64 -0
- package/dist/runtime/src/agents/claude-provider.ts +737 -0
- package/dist/runtime/src/agents/claude-registry.ts +115 -0
- package/dist/runtime/src/agents/codex-native-multi-agent.ts +177 -0
- package/dist/runtime/src/agents/codex-provider.ts +785 -0
- package/dist/runtime/src/agents/codex-registry.ts +115 -0
- package/dist/runtime/src/agents/codex-tree-kill.ts +365 -0
- package/dist/runtime/src/agents/contracts.ts +69 -0
- package/dist/runtime/src/agents/deep-map.ts +13 -0
- package/dist/runtime/src/agents/execution-host.ts +91 -0
- package/dist/runtime/src/agents/fixtures/claude-attachments.jsonl +2 -0
- package/dist/runtime/src/agents/fixtures/codex-shell.jsonl +5 -0
- package/dist/runtime/src/agents/fork.ts +65 -0
- package/dist/runtime/src/agents/hosted-agent.ts +48 -0
- package/dist/runtime/src/agents/idle-archiver.ts +128 -0
- package/dist/runtime/src/agents/index.ts +205 -0
- package/dist/runtime/src/agents/maestro-provider.ts +206 -0
- package/dist/runtime/src/agents/maestro-registry.ts +31 -0
- package/dist/runtime/src/agents/mcp-tools/ask-user.ts +280 -0
- package/dist/runtime/src/agents/mcp-tools/common.ts +21 -0
- package/dist/runtime/src/agents/mcp-tools/self-config.ts +220 -0
- package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +525 -0
- package/dist/runtime/src/agents/mcp-tools/visuals.ts +88 -0
- package/dist/runtime/src/agents/model-catalog.ts +151 -0
- package/dist/runtime/src/agents/registry.ts +24 -0
- package/dist/runtime/src/agents/rollout/claude.ts +331 -0
- package/dist/runtime/src/agents/rollout/codex.ts +569 -0
- package/dist/runtime/src/agents/rollout/shared.ts +205 -0
- package/dist/runtime/src/agents/self-config-core.ts +415 -0
- package/dist/runtime/src/agents/task-events.ts +44 -0
- package/dist/runtime/src/agents/tool-format.ts +352 -0
- package/dist/runtime/src/agents/topic-agent-switch.ts +193 -0
- package/dist/runtime/src/agents/topic-cleanup.ts +231 -0
- package/dist/runtime/src/agents/vault-tool-policy.ts +33 -0
- package/dist/runtime/src/application/execute-external-user-turn.ts +23 -0
- package/dist/runtime/src/application/submit-user-message.ts +47 -0
- package/dist/runtime/src/application/switch-topic-effort.ts +48 -0
- package/dist/runtime/src/application/switch-topic-model.ts +79 -0
- package/dist/runtime/src/application/topic-service.ts +139 -0
- package/dist/runtime/src/application/vault-command.ts +170 -0
- package/dist/runtime/src/bus.ts +319 -0
- package/dist/runtime/src/index.ts +333 -0
- package/dist/runtime/src/mcp/agent-health-server.ts +309 -0
- package/dist/runtime/src/mcp/background-bash-server.ts +346 -0
- package/dist/runtime/src/mcp/canonical-bridge-config.ts +84 -0
- package/dist/runtime/src/mcp/canonical-proxy-server.ts +171 -0
- package/dist/runtime/src/mcp/dm-ipc.ts +60 -0
- package/dist/runtime/src/mcp/mcp-helpers.ts +40 -0
- package/dist/runtime/src/mcp/paddleocr-server.ts +105 -0
- package/dist/runtime/src/mcp/peer-bridge.ts +114 -0
- package/dist/runtime/src/mcp/runtime-spec.ts +134 -0
- package/dist/runtime/src/mcp/session-comm/bridge-ipc-config.ts +27 -0
- package/dist/runtime/src/mcp/session-comm/peer-forward.ts +165 -0
- package/dist/runtime/src/mcp/session-comm/runtime.ts +115 -0
- package/dist/runtime/src/mcp/session-comm/server.ts +649 -0
- package/dist/runtime/src/mcp/session-comm/topic-config.ts +131 -0
- package/dist/runtime/src/mcp/session-comm/topics.ts +229 -0
- package/dist/runtime/src/mcp/stdio-protect.ts +84 -0
- package/dist/runtime/src/mcp/system-health-server.ts +130 -0
- package/dist/runtime/src/mcp/task-server.ts +173 -0
- package/dist/runtime/src/mcp/token-stats-server.ts +98 -0
- package/dist/runtime/src/mcp/vault-http.ts +219 -0
- package/dist/runtime/src/mcp/vault-run.ts +153 -0
- package/dist/runtime/src/mcp/vault-server.ts +104 -0
- package/dist/runtime/src/mcp/wiki-server.ts +755 -0
- package/dist/runtime/src/media/file-events.ts +33 -0
- package/dist/runtime/src/media/text-extractor.ts +312 -0
- package/dist/runtime/src/media/video.ts +92 -0
- package/dist/runtime/src/outbox/file-ops.ts +210 -0
- package/dist/runtime/src/outbox/index.ts +8 -0
- package/dist/runtime/src/outbox/utils.ts +51 -0
- package/dist/runtime/src/platform/background-bash/manager.ts +279 -0
- package/dist/runtime/src/platform/config.ts +371 -0
- package/dist/runtime/src/platform/constants.ts +18 -0
- package/dist/runtime/src/platform/debug-users.ts +14 -0
- package/dist/runtime/src/platform/delay.ts +1 -0
- package/dist/runtime/src/platform/error.ts +9 -0
- package/dist/runtime/src/platform/file-utils.ts +16 -0
- package/dist/runtime/src/platform/jsonl.ts +202 -0
- package/dist/runtime/src/platform/lifecycle.ts +174 -0
- package/dist/runtime/src/platform/logger.ts +31 -0
- package/dist/runtime/src/platform/maestro-bootstrap-env.ts +3 -0
- package/dist/runtime/src/platform/mcp-config.ts +865 -0
- package/dist/runtime/src/platform/modules.ts +145 -0
- package/dist/runtime/src/platform/node-plugins.ts +46 -0
- package/dist/runtime/src/platform/playwright/manager.ts +1233 -0
- package/dist/runtime/src/prompts/agents/wiki-archiver.md +261 -0
- package/dist/runtime/src/prompts/builders.ts +357 -0
- package/dist/runtime/src/prompts/sessions/channel-system.md +48 -0
- package/dist/runtime/src/prompts/sessions/manager-system.md +37 -0
- package/dist/runtime/src/prompts/sessions/topic-system.md +47 -0
- package/dist/runtime/src/prompts/sessions/visual-design.md +86 -0
- package/dist/runtime/src/query/active-rooms.ts +532 -0
- package/dist/runtime/src/query/control.ts +313 -0
- package/dist/runtime/src/query/index.ts +24 -0
- package/dist/runtime/src/query/session-inbox-cleanup.ts +44 -0
- package/dist/runtime/src/query/session-inbox-path.ts +42 -0
- package/dist/runtime/src/query/state.ts +94 -0
- package/dist/runtime/src/query/types.ts +83 -0
- package/dist/runtime/src/runtime/ask-callbacks.ts +153 -0
- package/dist/runtime/src/runtime/attachments.ts +151 -0
- package/dist/runtime/src/runtime/background-sessions.ts +83 -0
- package/dist/runtime/src/runtime/channel-context.ts +107 -0
- package/dist/runtime/src/runtime/errors.ts +69 -0
- package/dist/runtime/src/runtime/event-heartbeat.ts +77 -0
- package/dist/runtime/src/runtime/file-hooks.ts +81 -0
- package/dist/runtime/src/runtime/footer.ts +23 -0
- package/dist/runtime/src/runtime/inbox.ts +946 -0
- package/dist/runtime/src/runtime/tasks.ts +56 -0
- package/dist/runtime/src/runtime/topic-config.ts +29 -0
- package/dist/runtime/src/runtime/turn-runner.ts +2286 -0
- package/dist/runtime/src/runtime/usage-alert.ts +84 -0
- package/dist/runtime/src/runtime/visual-html.ts +109 -0
- package/dist/runtime/src/runtime/visual-store.ts +430 -0
- package/dist/runtime/src/runtime/visuals.ts +171 -0
- package/dist/runtime/src/security/sanitize.ts +27 -0
- package/dist/runtime/src/security/sensitive-path.ts +34 -0
- package/dist/runtime/src/storage/activity-log.ts +152 -0
- package/dist/runtime/src/storage/api-messages.ts +617 -0
- package/dist/runtime/src/storage/api-topic-brief.ts +127 -0
- package/dist/runtime/src/storage/api-topic-config.ts +192 -0
- package/dist/runtime/src/storage/api-topics.ts +844 -0
- package/dist/runtime/src/storage/app-settings.ts +44 -0
- package/dist/runtime/src/storage/browser-profiles.ts +133 -0
- package/dist/runtime/src/storage/conversations.ts +268 -0
- package/dist/runtime/src/storage/forum/agent-settings.ts +20 -0
- package/dist/runtime/src/storage/forum/index.ts +4 -0
- package/dist/runtime/src/storage/forum/repository.ts +283 -0
- package/dist/runtime/src/storage/forum/schema.ts +300 -0
- package/dist/runtime/src/storage/forum-db.ts +23 -0
- package/dist/runtime/src/storage/runtime-events.ts +130 -0
- package/dist/runtime/src/storage/runtime-leases.ts +189 -0
- package/dist/runtime/src/storage/runtime-process-leases.ts +216 -0
- package/dist/runtime/src/storage/runtime-topic-state.ts +148 -0
- package/dist/runtime/src/storage/runtime-turn-requests.ts +300 -0
- package/dist/runtime/src/storage/self-schedules.ts +298 -0
- package/dist/runtime/src/storage/session-asks.ts +347 -0
- package/dist/runtime/src/storage/sqlite.ts +129 -0
- package/dist/runtime/src/storage/tasks.ts +179 -0
- package/dist/runtime/src/storage/token-stats.ts +144 -0
- package/dist/runtime/src/storage/topic-archive-state.ts +59 -0
- package/dist/runtime/src/storage/topic-archive.ts +74 -0
- package/dist/runtime/src/storage/topic-settings.ts +67 -0
- package/dist/runtime/src/storage/topic-transcript.ts +98 -0
- package/dist/runtime/src/storage/vault-crypto.ts +65 -0
- package/dist/runtime/src/storage/vault.ts +225 -0
- package/dist/runtime/src/storage/wiki-summary-names.ts +33 -0
- package/dist/runtime/src/storage/wiki.ts +85 -0
- package/dist/runtime/src/topics/create.ts +123 -0
- package/dist/runtime/src/topics/derive.ts +398 -0
- package/dist/runtime/src/topics/lifecycle.ts +311 -0
- package/dist/runtime/src/topics/links.ts +7 -0
- package/dist/runtime/src/topics/personal-general.ts +65 -0
- package/dist/runtime/src/topics/session.ts +381 -0
- package/dist/runtime/src/types/api.ts +497 -0
- package/dist/runtime/src/types.ts +255 -0
- package/dist/runtime/src/version.ts +1 -0
- package/dist/runtime/tsconfig.json +13 -0
- package/package.json +16 -3
- package/dist/main.d.ts +0 -3
- package/dist/main.d.ts.map +0 -1
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import type {
|
|
4
|
+
HookInput,
|
|
5
|
+
Options,
|
|
6
|
+
PostToolUseHookInput,
|
|
7
|
+
PreToolUseHookInput,
|
|
8
|
+
SDKAssistantMessage,
|
|
9
|
+
SDKPartialAssistantMessage,
|
|
10
|
+
SDKResultMessage,
|
|
11
|
+
SDKSystemMessage,
|
|
12
|
+
SDKTaskStartedMessage,
|
|
13
|
+
SDKToolProgressMessage,
|
|
14
|
+
SDKToolUseSummaryMessage,
|
|
15
|
+
SDKUserMessage,
|
|
16
|
+
SDKUserMessageReplay,
|
|
17
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
18
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
19
|
+
import type {
|
|
20
|
+
ContentBlockParam,
|
|
21
|
+
ImageBlockParam,
|
|
22
|
+
} from "@anthropic-ai/sdk/resources/messages/messages.mjs";
|
|
23
|
+
import { claudeRegistry } from "#agents/claude-registry";
|
|
24
|
+
import { deepMapStrings } from "#agents/deep-map";
|
|
25
|
+
import {
|
|
26
|
+
hostedClaudeCodeExecutablePath,
|
|
27
|
+
hostedMcpServers,
|
|
28
|
+
redactHostedSecrets,
|
|
29
|
+
referencesHostedSecretStorage,
|
|
30
|
+
shouldRedirectHostedVaultTool,
|
|
31
|
+
} from "#agents/execution-host";
|
|
32
|
+
import { VAULT_BROKER_REDIRECT_ERROR } from "#agents/vault-tool-policy";
|
|
33
|
+
import { extractFileEvents } from "#media/file-events";
|
|
34
|
+
import { errMsg } from "#platform/error";
|
|
35
|
+
import { logger } from "#platform/logger";
|
|
36
|
+
import type { AgentInputAttachment, AgentQueryOptions, EffortLevel, UnifiedEvent } from "#types";
|
|
37
|
+
|
|
38
|
+
const CLAUDE_DEFAULT_DISALLOWED_TOOLS = [
|
|
39
|
+
"AskUserQuestion",
|
|
40
|
+
"Workflow",
|
|
41
|
+
"TodoWrite",
|
|
42
|
+
"TaskCreate",
|
|
43
|
+
"TaskUpdate",
|
|
44
|
+
"TaskList",
|
|
45
|
+
"TaskGet",
|
|
46
|
+
"Task",
|
|
47
|
+
"Agent",
|
|
48
|
+
"TaskOutput",
|
|
49
|
+
"TaskStop",
|
|
50
|
+
] as const;
|
|
51
|
+
|
|
52
|
+
export function buildClaudeDisallowedTools(
|
|
53
|
+
extra: readonly string[] | undefined = undefined,
|
|
54
|
+
): string[] {
|
|
55
|
+
return [...new Set([...CLAUDE_DEFAULT_DISALLOWED_TOOLS, ...(extra ?? [])])];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Narrow our shared `EffortLevel` to the subset claude-agent-sdk accepts.
|
|
60
|
+
* Claude SDK rejects "minimal" (codex-only); we drop it silently rather than
|
|
61
|
+
* coercing, matching our policy for unrecognized model strings.
|
|
62
|
+
*/
|
|
63
|
+
type ClaudeEffort = Exclude<EffortLevel, "minimal">;
|
|
64
|
+
function toClaudeEffort(e: EffortLevel | undefined): ClaudeEffort | undefined {
|
|
65
|
+
if (!e || !claudeRegistry.validateEffort(e)) return undefined;
|
|
66
|
+
return e as ClaudeEffort;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type ClaudeImageMimeType = "image/jpeg" | "image/png" | "image/gif" | "image/webp";
|
|
70
|
+
|
|
71
|
+
const CLAUDE_IMAGE_MAX_BYTES = 10 * 1024 * 1024;
|
|
72
|
+
const CLAUDE_IMAGE_MIME_TYPES = new Set<string>([
|
|
73
|
+
"image/jpeg",
|
|
74
|
+
"image/png",
|
|
75
|
+
"image/gif",
|
|
76
|
+
"image/webp",
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
function isClaudeImageAttachment(
|
|
80
|
+
attachment: AgentInputAttachment,
|
|
81
|
+
): attachment is AgentInputAttachment & { mimeType: ClaudeImageMimeType } {
|
|
82
|
+
return attachment.type === "image" && CLAUDE_IMAGE_MIME_TYPES.has(attachment.mimeType);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function* singleUserMessage(message: SDKUserMessage): AsyncGenerator<SDKUserMessage> {
|
|
86
|
+
yield message;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildClaudePrompt(opts: AgentQueryOptions): string | AsyncIterable<SDKUserMessage> {
|
|
90
|
+
const attachments = opts.attachments ?? [];
|
|
91
|
+
if (attachments.length === 0) return opts.prompt;
|
|
92
|
+
|
|
93
|
+
const imageBlocks: ImageBlockParam[] = [];
|
|
94
|
+
for (const attachment of attachments) {
|
|
95
|
+
if (!isClaudeImageAttachment(attachment)) continue;
|
|
96
|
+
if (attachment.sizeBytes > CLAUDE_IMAGE_MAX_BYTES) {
|
|
97
|
+
logger.info(
|
|
98
|
+
{
|
|
99
|
+
path: attachment.path,
|
|
100
|
+
mimeType: attachment.mimeType,
|
|
101
|
+
sizeBytes: attachment.sizeBytes,
|
|
102
|
+
maxBytes: CLAUDE_IMAGE_MAX_BYTES,
|
|
103
|
+
},
|
|
104
|
+
"claudeProvider: skipping oversized image attachment content block",
|
|
105
|
+
);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
imageBlocks.push({
|
|
111
|
+
type: "image",
|
|
112
|
+
source: {
|
|
113
|
+
type: "base64",
|
|
114
|
+
media_type: attachment.mimeType,
|
|
115
|
+
data: readFileSync(attachment.path).toString("base64"),
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
} catch (err) {
|
|
119
|
+
logger.warn(
|
|
120
|
+
{
|
|
121
|
+
path: attachment.path,
|
|
122
|
+
mimeType: attachment.mimeType,
|
|
123
|
+
err,
|
|
124
|
+
},
|
|
125
|
+
"claudeProvider: failed to read image attachment for content block",
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (imageBlocks.length === 0) return opts.prompt;
|
|
131
|
+
|
|
132
|
+
const content: ContentBlockParam[] = [{ type: "text", text: opts.prompt }, ...imageBlocks];
|
|
133
|
+
return singleUserMessage({
|
|
134
|
+
type: "user",
|
|
135
|
+
message: {
|
|
136
|
+
role: "user",
|
|
137
|
+
content,
|
|
138
|
+
},
|
|
139
|
+
parent_tool_use_id: null,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// --- Stream event sub-types (from @anthropic-ai/sdk, not directly importable) ---
|
|
144
|
+
interface ContentBlockDelta {
|
|
145
|
+
type: "content_block_delta";
|
|
146
|
+
delta: { type: string; partial_json?: string; text?: string };
|
|
147
|
+
}
|
|
148
|
+
type StreamEvent = ContentBlockDelta | { type: string };
|
|
149
|
+
|
|
150
|
+
// --- Assistant message content block types (from BetaMessage.content) ---
|
|
151
|
+
interface TextBlock {
|
|
152
|
+
type: "text";
|
|
153
|
+
text: string;
|
|
154
|
+
}
|
|
155
|
+
interface ToolUseBlock {
|
|
156
|
+
type: "tool_use";
|
|
157
|
+
id: string;
|
|
158
|
+
name: string;
|
|
159
|
+
input: Record<string, unknown>;
|
|
160
|
+
}
|
|
161
|
+
interface ToolResultBlock {
|
|
162
|
+
type: "tool_result";
|
|
163
|
+
tool_use_id: string;
|
|
164
|
+
content: string | unknown;
|
|
165
|
+
}
|
|
166
|
+
type ContentBlock = TextBlock | ToolUseBlock | ToolResultBlock | { type: string };
|
|
167
|
+
|
|
168
|
+
// --- Process tree kill on abort ---
|
|
169
|
+
//
|
|
170
|
+
// The Claude Agent SDK spawns a `claude` CLI subprocess, which in turn spawns
|
|
171
|
+
// MCP servers (playwright-mcp, python OCR servers, …) which themselves spawn
|
|
172
|
+
// browsers / interpreters. The SDK's default abort path only signals the
|
|
173
|
+
// direct child, leaving grandchildren orphaned — over time they accumulate as
|
|
174
|
+
// zombies, holding ports (Playwright DevTools 9222) and leaking memory
|
|
175
|
+
// (Chrome ~hundreds of MB per orphan). Single-user Mac deployments notice
|
|
176
|
+
// this within a day of repeated /abort.
|
|
177
|
+
//
|
|
178
|
+
// We override `spawnClaudeCodeProcess` to:
|
|
179
|
+
// 1. spawn with `detached: true` → child becomes the leader of a new
|
|
180
|
+
// process group with the same pgid as its pid;
|
|
181
|
+
// 2. on abort, send the signal to `-pid` (the whole group) instead of the
|
|
182
|
+
// single pid, so all transitive descendants receive it;
|
|
183
|
+
// 3. escalate to SIGKILL after a 2.5s grace if SIGTERM didn't take.
|
|
184
|
+
|
|
185
|
+
type SpawnClaudeCodeProcess = NonNullable<Options["spawnClaudeCodeProcess"]>;
|
|
186
|
+
type SpawnClaudeCodeOptions = Parameters<SpawnClaudeCodeProcess>[0];
|
|
187
|
+
type SpawnClaudeCodeResult = ReturnType<SpawnClaudeCodeProcess>;
|
|
188
|
+
|
|
189
|
+
const CLAUDE_ABORT_SIGKILL_DELAY_MS = 2500;
|
|
190
|
+
|
|
191
|
+
function signalProcessTree(pid: number, signal: NodeJS.Signals): boolean {
|
|
192
|
+
// Negative pid = "signal every process in this process group". Falls back
|
|
193
|
+
// to single-pid signaling if the group call fails (e.g. on platforms where
|
|
194
|
+
// `detached` didn't materialize a new group).
|
|
195
|
+
try {
|
|
196
|
+
process.kill(-pid, signal);
|
|
197
|
+
return true;
|
|
198
|
+
} catch (groupErr) {
|
|
199
|
+
try {
|
|
200
|
+
process.kill(pid, signal);
|
|
201
|
+
return true;
|
|
202
|
+
} catch (pidErr) {
|
|
203
|
+
logger.debug({ groupErr, pidErr, pid, signal }, "Failed to signal Claude process tree");
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function spawnClaudeCodeProcessWithTreeKill(
|
|
210
|
+
options: SpawnClaudeCodeOptions,
|
|
211
|
+
): SpawnClaudeCodeResult {
|
|
212
|
+
const child = spawn(options.command, options.args, {
|
|
213
|
+
cwd: options.cwd,
|
|
214
|
+
detached: true,
|
|
215
|
+
env: options.env as NodeJS.ProcessEnv,
|
|
216
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
217
|
+
});
|
|
218
|
+
if (!child.stdin || !child.stdout) {
|
|
219
|
+
child.kill("SIGKILL");
|
|
220
|
+
throw new Error("Failed to spawn Claude Code process with piped stdin/stdout");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let exited = false;
|
|
224
|
+
let killTimer: NodeJS.Timeout | undefined;
|
|
225
|
+
const clearKillTimer = () => {
|
|
226
|
+
if (killTimer) clearTimeout(killTimer);
|
|
227
|
+
killTimer = undefined;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const kill = (signal: NodeJS.Signals): boolean => {
|
|
231
|
+
if (!child.pid) return child.kill(signal);
|
|
232
|
+
const ok = signalProcessTree(child.pid, signal);
|
|
233
|
+
if (signal !== "SIGKILL") {
|
|
234
|
+
// Soft signal: schedule a SIGKILL fallback so a stuck process tree
|
|
235
|
+
// can't survive abort indefinitely.
|
|
236
|
+
clearKillTimer();
|
|
237
|
+
killTimer = setTimeout(() => {
|
|
238
|
+
if (!exited && child.pid) {
|
|
239
|
+
logger.warn({ pid: child.pid }, "Claude process tree still alive after SIGTERM; SIGKILL");
|
|
240
|
+
signalProcessTree(child.pid, "SIGKILL");
|
|
241
|
+
}
|
|
242
|
+
}, CLAUDE_ABORT_SIGKILL_DELAY_MS);
|
|
243
|
+
killTimer.unref?.();
|
|
244
|
+
}
|
|
245
|
+
return ok;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const onAbort = () => {
|
|
249
|
+
logger.warn({ pid: child.pid, command: options.command }, "Aborting Claude Code process tree");
|
|
250
|
+
kill("SIGTERM");
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
if (options.signal.aborted) onAbort();
|
|
254
|
+
else options.signal.addEventListener("abort", onAbort, { once: true });
|
|
255
|
+
|
|
256
|
+
child.once("exit", (code, signal) => {
|
|
257
|
+
exited = true;
|
|
258
|
+
clearKillTimer();
|
|
259
|
+
options.signal.removeEventListener("abort", onAbort);
|
|
260
|
+
logger.debug({ pid: child.pid, code, signal }, "Claude Code process exited");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Consume the child's 'error' event. Without a listener, a spawn-level error
|
|
264
|
+
// propagates into the SDK's readline-over-stdout teardown and crashes the
|
|
265
|
+
// whole process with "this.input.pause is not a function" (node:readline
|
|
266
|
+
// close→pause on a stream that never initialized). Handling it here keeps the
|
|
267
|
+
// failure local and surfaces a clean diagnostic.
|
|
268
|
+
child.once("error", (err) => {
|
|
269
|
+
exited = true;
|
|
270
|
+
clearKillTimer();
|
|
271
|
+
options.signal.removeEventListener("abort", onAbort);
|
|
272
|
+
logger.error(
|
|
273
|
+
{
|
|
274
|
+
pid: child.pid,
|
|
275
|
+
command: options.command,
|
|
276
|
+
err: err instanceof Error ? err.message : String(err),
|
|
277
|
+
},
|
|
278
|
+
"Claude Code process error event",
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
stdin: child.stdin,
|
|
284
|
+
stdout: child.stdout,
|
|
285
|
+
get killed() {
|
|
286
|
+
return child.killed || exited;
|
|
287
|
+
},
|
|
288
|
+
get exitCode() {
|
|
289
|
+
return child.exitCode;
|
|
290
|
+
},
|
|
291
|
+
kill,
|
|
292
|
+
on(event, listener) {
|
|
293
|
+
child.on(event, listener);
|
|
294
|
+
},
|
|
295
|
+
once(event, listener) {
|
|
296
|
+
child.once(event, listener);
|
|
297
|
+
},
|
|
298
|
+
off(event, listener) {
|
|
299
|
+
child.off(event, listener);
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Recognize the multiple shapes Node + WHATWG fetch use when a request is
|
|
306
|
+
* cancelled via `AbortController`. Mirrors `isAbortError` in maestro/provider
|
|
307
|
+
* — both files keep their own copy so neither has to import the other (the
|
|
308
|
+
* agent providers are deliberately siblings, no shared error helpers).
|
|
309
|
+
*/
|
|
310
|
+
function isAbortError(err: unknown): boolean {
|
|
311
|
+
if (!err || typeof err !== "object") return false;
|
|
312
|
+
const e = err as { name?: unknown; code?: unknown };
|
|
313
|
+
if (e.name === "AbortError") return true;
|
|
314
|
+
if (e.code === 20 || e.code === "ABORT_ERR") return true;
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Core Claude provider.
|
|
320
|
+
* Wraps the @anthropic-ai/claude-agent-sdk into a UnifiedEvent stream so any
|
|
321
|
+
* caller can consume it identically to other providers such as codexProvider.
|
|
322
|
+
* The dispatch entry point is `runAgent` in agents/index.ts.
|
|
323
|
+
*
|
|
324
|
+
* Failure handling: any throw escaping the SDK iteration (missing Claude CLI
|
|
325
|
+
* binary, OAuth not configured, transient network) is caught and yielded as
|
|
326
|
+
* a `{type:"error"}` UnifiedEvent so the dispatcher never sees a synthetic
|
|
327
|
+
* crash and event-processor's `error` case classifies it with the right
|
|
328
|
+
* provider name. Matches the graceful pattern maestroProvider already uses.
|
|
329
|
+
* Abort signals are swallowed silently (the user moved on) so they don't
|
|
330
|
+
* surface as ghost error messages.
|
|
331
|
+
*/
|
|
332
|
+
export async function* claudeProvider(opts: AgentQueryOptions): AsyncGenerator<UnifiedEvent> {
|
|
333
|
+
const claudeExecutable = hostedClaudeCodeExecutablePath();
|
|
334
|
+
// Up-front existence check — gives the user a clean, actionable error
|
|
335
|
+
// instead of waiting for the SDK to spawn the binary and fail mid-stream
|
|
336
|
+
// with an opaque ENOENT. The SDK's own resolution path runs after spawn,
|
|
337
|
+
// so if CLAUDE_EXECUTABLE points nowhere we want to surface it now.
|
|
338
|
+
if (claudeExecutable && !existsSync(claudeExecutable)) {
|
|
339
|
+
yield {
|
|
340
|
+
type: "error",
|
|
341
|
+
content: `Claude CLI not found at ${claudeExecutable}. Install Claude Code or configure the executable path.`,
|
|
342
|
+
};
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const cleanEnv = { ...process.env };
|
|
347
|
+
delete cleanEnv.NEGOTIUM_PEER_SESSION_BRIDGE_URL;
|
|
348
|
+
delete cleanEnv.NEGOTIUM_PEER_SESSION_BRIDGE_TOKEN;
|
|
349
|
+
delete cleanEnv.CLAUDECODE;
|
|
350
|
+
cleanEnv.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT ??= process.env.PADDLEOCR_TIMEOUT_MS ?? "300000";
|
|
351
|
+
// Workflow is a feature flag, not a disallowedTools-addressable tool name.
|
|
352
|
+
// The env var is the version-robust kill switch (works on any CLI the SDK
|
|
353
|
+
// spawns); the `settings` flag layer below covers it a second time.
|
|
354
|
+
cleanEnv.CLAUDE_CODE_DISABLE_WORKFLOWS = "1";
|
|
355
|
+
|
|
356
|
+
const queryOptions: Options = {
|
|
357
|
+
...(claudeExecutable ? { pathToClaudeCodeExecutable: claudeExecutable } : {}),
|
|
358
|
+
spawnClaudeCodeProcess: spawnClaudeCodeProcessWithTreeKill,
|
|
359
|
+
cwd: opts.cwd,
|
|
360
|
+
permissionMode: "bypassPermissions",
|
|
361
|
+
allowDangerouslySkipPermissions: true,
|
|
362
|
+
includePartialMessages: true,
|
|
363
|
+
env: cleanEnv,
|
|
364
|
+
mcpServers: hostedMcpServers(opts) as Options["mcpServers"],
|
|
365
|
+
abortController: opts.abortController,
|
|
366
|
+
// `AskUserQuestion` is Claude Code's built-in clarification tool, but it
|
|
367
|
+
// expects the SDK CLI to render a TUI prompt and read stdin for the
|
|
368
|
+
// answer — neither exists in this headless Telegram bridge. Without
|
|
369
|
+
// disallowing it, the model would call AskUserQuestion, the SDK would
|
|
370
|
+
// wait for a TUI response that never comes, and the turn would hang
|
|
371
|
+
// until abort. Forbidding the tool nudges the model to ask the same
|
|
372
|
+
// clarification as plain text (see topic-system prompt), which routes
|
|
373
|
+
// through the normal next-user-message path and the user just answers
|
|
374
|
+
// in the topic.
|
|
375
|
+
//
|
|
376
|
+
// Claude Code's private task store, Workflow, and native subagent tools are
|
|
377
|
+
// not Negotium state. They stay disabled even when a caller supplies custom
|
|
378
|
+
// agent definitions: delegation must go through runtime.spawn_subagent and
|
|
379
|
+
// task state must go through the required shared task MCP.
|
|
380
|
+
disallowedTools: buildClaudeDisallowedTools(opts.disallowedTools),
|
|
381
|
+
...(opts.model ? { model: opts.model } : {}),
|
|
382
|
+
...(opts.maxBudgetUsd ? { maxBudgetUsd: opts.maxBudgetUsd } : {}),
|
|
383
|
+
...(opts.agents
|
|
384
|
+
? {
|
|
385
|
+
agents: Object.fromEntries(
|
|
386
|
+
Object.entries(opts.agents).map(([name, def]) => {
|
|
387
|
+
const { effort, ...rest } = def;
|
|
388
|
+
const narrowed = typeof effort === "string" ? toClaudeEffort(effort) : effort;
|
|
389
|
+
return [name, narrowed !== undefined ? { ...rest, effort: narrowed } : rest];
|
|
390
|
+
}),
|
|
391
|
+
),
|
|
392
|
+
}
|
|
393
|
+
: {}),
|
|
394
|
+
...(toClaudeEffort(opts.effort) ? { effort: toClaudeEffort(opts.effort) } : {}),
|
|
395
|
+
hooks: {
|
|
396
|
+
PreToolUse: [
|
|
397
|
+
{
|
|
398
|
+
hooks: [
|
|
399
|
+
async (input: HookInput) => {
|
|
400
|
+
const { tool_name, tool_input } = input as PreToolUseHookInput;
|
|
401
|
+
if (referencesHostedSecretStorage(tool_input)) {
|
|
402
|
+
return {
|
|
403
|
+
hookSpecificOutput: {
|
|
404
|
+
hookEventName: "PreToolUse" as const,
|
|
405
|
+
permissionDecision: "deny" as const,
|
|
406
|
+
permissionDecisionReason: "Runtime secret storage access is not permitted",
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Block built-in run_in_background: the bash runs inside the CLI
|
|
412
|
+
// subprocess and dies when the turn ends (SIGTERM on IPC close).
|
|
413
|
+
// Direct the model to the MCP tool that survives across turns and
|
|
414
|
+
// auto-injects a completion notification via session-inbox.
|
|
415
|
+
if (
|
|
416
|
+
tool_name === "Bash" &&
|
|
417
|
+
(tool_input as Record<string, unknown>)?.run_in_background === true
|
|
418
|
+
) {
|
|
419
|
+
return {
|
|
420
|
+
hookSpecificOutput: {
|
|
421
|
+
hookEventName: "PreToolUse" as const,
|
|
422
|
+
permissionDecision: "deny" as const,
|
|
423
|
+
permissionDecisionReason:
|
|
424
|
+
"run_in_background is not supported in headless mode — the bash process dies when the agent turn ends. Use mcp__background-bash__background_bash_run instead: it survives across turns and automatically injects the result into the session when done.",
|
|
425
|
+
},
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Detect real Vault placeholders but never inject plaintext into
|
|
430
|
+
// provider-visible tool input. The Vault MCP broker expands them.
|
|
431
|
+
const userId = opts.userId ?? "";
|
|
432
|
+
if (!shouldRedirectHostedVaultTool(userId, tool_name, tool_input)) {
|
|
433
|
+
return { continue: true };
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
hookSpecificOutput: {
|
|
437
|
+
hookEventName: "PreToolUse" as const,
|
|
438
|
+
permissionDecision: "deny" as const,
|
|
439
|
+
permissionDecisionReason: VAULT_BROKER_REDIRECT_ERROR,
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
},
|
|
443
|
+
],
|
|
444
|
+
},
|
|
445
|
+
],
|
|
446
|
+
PostToolUse: [
|
|
447
|
+
{
|
|
448
|
+
hooks: [
|
|
449
|
+
async (input: HookInput) => {
|
|
450
|
+
const { tool_response } = input as PostToolUseHookInput;
|
|
451
|
+
const userId = opts.userId ?? "";
|
|
452
|
+
const redacted = deepMapStrings(tool_response, (value) =>
|
|
453
|
+
redactHostedSecrets(userId, value),
|
|
454
|
+
);
|
|
455
|
+
if (JSON.stringify(redacted) === JSON.stringify(tool_response)) {
|
|
456
|
+
return { continue: true };
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
hookSpecificOutput: {
|
|
460
|
+
hookEventName: "PostToolUse" as const,
|
|
461
|
+
updatedToolOutput: redacted,
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
},
|
|
467
|
+
],
|
|
468
|
+
},
|
|
469
|
+
settingSources: ["project"] as Options["settingSources"],
|
|
470
|
+
// Disable the multi-agent Workflow feature: a headless backend query has
|
|
471
|
+
// no business fanning out orchestration subagents, and the "ultracode"
|
|
472
|
+
// keyword trigger would let user-authored message text opt into it.
|
|
473
|
+
settings: {
|
|
474
|
+
disableWorkflows: true,
|
|
475
|
+
workflowKeywordTriggerEnabled: false,
|
|
476
|
+
},
|
|
477
|
+
systemPrompt: opts.systemPrompt,
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
if (opts.sessionId) {
|
|
481
|
+
queryOptions.resume = opts.sessionId;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Wrap the SDK iteration so init failures (missing claude CLI, OAuth not
|
|
485
|
+
// configured, transient network errors before the first event) surface as
|
|
486
|
+
// a normal error UnifiedEvent. Without this, an unconfigured Claude
|
|
487
|
+
// install would crash the dispatcher with a synthetic exception and the
|
|
488
|
+
// user-facing classifyError would still fire — but only via the handler
|
|
489
|
+
// catch path, which means cleanup ordering differed from maestro/codex.
|
|
490
|
+
// Keeping the failure shape consistent across providers simplifies the
|
|
491
|
+
// upstream error code.
|
|
492
|
+
// Thinking heartbeat: long extended-thinking stretches (minutes on fable /
|
|
493
|
+
// high effort) emit no text or tool events, so the room UI goes silent and
|
|
494
|
+
// the user can't tell whether the turn is still alive. The SDK does stream
|
|
495
|
+
// thinking signals (`thinking_delta` content deltas, `system/thinking_tokens`
|
|
496
|
+
// pings during redacted thinking) — digest them into a throttled
|
|
497
|
+
// `tool_progress` heartbeat that the client already renders as a live status
|
|
498
|
+
// chip (its handler also re-asserts the typing indicator).
|
|
499
|
+
const THINKING_BEAT_MS = 5000;
|
|
500
|
+
let thinkingStart: number | null = null;
|
|
501
|
+
let lastThinkingBeat = 0;
|
|
502
|
+
let lastContextTokens: number | undefined;
|
|
503
|
+
let lastContextModel: string | undefined;
|
|
504
|
+
const thinkingBeat = (): { type: "tool_progress"; toolName: string; elapsed: number } | null => {
|
|
505
|
+
const now = Date.now();
|
|
506
|
+
if (thinkingStart === null) {
|
|
507
|
+
thinkingStart = now;
|
|
508
|
+
lastThinkingBeat = now;
|
|
509
|
+
return { type: "tool_progress", toolName: "thinking", elapsed: 0 };
|
|
510
|
+
}
|
|
511
|
+
if (now - lastThinkingBeat < THINKING_BEAT_MS) return null;
|
|
512
|
+
lastThinkingBeat = now;
|
|
513
|
+
return { type: "tool_progress", toolName: "thinking", elapsed: (now - thinkingStart) / 1000 };
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
try {
|
|
517
|
+
const prompt = buildClaudePrompt(opts);
|
|
518
|
+
for await (const message of query({
|
|
519
|
+
prompt,
|
|
520
|
+
options: queryOptions,
|
|
521
|
+
})) {
|
|
522
|
+
// --- Thinking heartbeat (see above) ---
|
|
523
|
+
// A thinking "stretch" ends at the first non-thinking signal so elapsed
|
|
524
|
+
// restarts cleanly on the next thinking phase within the same turn.
|
|
525
|
+
if (
|
|
526
|
+
message.type === "system" &&
|
|
527
|
+
(message as { subtype?: string }).subtype === "thinking_tokens"
|
|
528
|
+
) {
|
|
529
|
+
const beat = thinkingBeat();
|
|
530
|
+
if (beat) yield beat;
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// --- Stream events (token-by-token) ---
|
|
535
|
+
// tool_use is NOT emitted from the stream path: the completed assistant
|
|
536
|
+
// message below carries the same block with its input already parsed,
|
|
537
|
+
// and tools only start executing after the message completes — so the
|
|
538
|
+
// stream copy added no timeliness, only a duplicate tool_use per turn
|
|
539
|
+
// in the unified log (doubling `<!-- Tool: ... -->` comments when
|
|
540
|
+
// bridging to another agent).
|
|
541
|
+
if (message.type === "stream_event") {
|
|
542
|
+
const streamMsg = message as SDKPartialAssistantMessage;
|
|
543
|
+
const evt = (streamMsg as { event?: StreamEvent }).event;
|
|
544
|
+
if (!evt) continue;
|
|
545
|
+
|
|
546
|
+
if (evt.type === "content_block_delta") {
|
|
547
|
+
const delta = evt as ContentBlockDelta;
|
|
548
|
+
if (delta.delta.type === "thinking_delta") {
|
|
549
|
+
const beat = thinkingBeat();
|
|
550
|
+
if (beat) yield beat;
|
|
551
|
+
} else if (delta.delta.type === "text_delta" && delta.delta.text) {
|
|
552
|
+
// Claude only exposes tool_use blocks on the completed assistant
|
|
553
|
+
// message below. Forwarding text deltas here would place all text
|
|
554
|
+
// before those tools, making the original block order impossible
|
|
555
|
+
// for consumers to reconstruct. The completed message emits text
|
|
556
|
+
// and tool blocks in their canonical order.
|
|
557
|
+
thinkingStart = null;
|
|
558
|
+
} else {
|
|
559
|
+
// Tool-args streaming (partial_json) etc. — thinking stretch over.
|
|
560
|
+
thinkingStart = null;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Any completed message (assistant text, tool events, results) ends the
|
|
568
|
+
// current thinking stretch.
|
|
569
|
+
thinkingStart = null;
|
|
570
|
+
|
|
571
|
+
// --- Tool progress ---
|
|
572
|
+
if (message.type === "tool_progress") {
|
|
573
|
+
const m = message as SDKToolProgressMessage;
|
|
574
|
+
yield {
|
|
575
|
+
type: "tool_progress",
|
|
576
|
+
toolName: m.tool_name,
|
|
577
|
+
elapsed: m.elapsed_time_seconds,
|
|
578
|
+
};
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// --- Tool use summary ---
|
|
583
|
+
if (message.type === "tool_use_summary") {
|
|
584
|
+
const m = message as SDKToolUseSummaryMessage;
|
|
585
|
+
yield { type: "tool_use_summary", summary: m.summary };
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// --- System init ---
|
|
590
|
+
if (message.type === "system") {
|
|
591
|
+
const m = message as SDKSystemMessage;
|
|
592
|
+
if (m.subtype === "init") {
|
|
593
|
+
yield { type: "session", sessionId: m.session_id };
|
|
594
|
+
} else if (m.subtype === "task_started") {
|
|
595
|
+
const t = m as unknown as SDKTaskStartedMessage;
|
|
596
|
+
if (t.subagent_type && !t.skip_transcript) {
|
|
597
|
+
const desc = t.description ? ` ${t.description.slice(0, 60)}` : "";
|
|
598
|
+
yield { type: "status", content: `▶ [${t.subagent_type}]${desc}` };
|
|
599
|
+
}
|
|
600
|
+
} else {
|
|
601
|
+
// Log all other system messages (task_updated, task_progress,
|
|
602
|
+
// task_notification, etc.) so we can observe lifecycle events.
|
|
603
|
+
logger.debug(
|
|
604
|
+
{ subtype: m.subtype, msg: m },
|
|
605
|
+
"claudeProvider: system message (unhandled subtype)",
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// --- Result (terminal) ---
|
|
612
|
+
// SDK occasionally yields stray messages after a `result`; consuming them
|
|
613
|
+
// surfaces as ghost messages in the topic ("어? 뭐가 더 나오네?"). Close
|
|
614
|
+
// the generator immediately on terminal so the SDK stream is dropped and
|
|
615
|
+
// any trailing payloads can't leak through.
|
|
616
|
+
if (message.type === "result") {
|
|
617
|
+
const m = message as SDKResultMessage;
|
|
618
|
+
if (m.subtype === "success") {
|
|
619
|
+
const contextWindow =
|
|
620
|
+
(lastContextModel ? m.modelUsage?.[lastContextModel]?.contextWindow : undefined) ??
|
|
621
|
+
Object.values(m.modelUsage ?? {})[0]?.contextWindow;
|
|
622
|
+
yield {
|
|
623
|
+
type: "result",
|
|
624
|
+
content: m.result,
|
|
625
|
+
stopReason: m.stop_reason ?? "end_turn",
|
|
626
|
+
usage: m.usage
|
|
627
|
+
? {
|
|
628
|
+
inputTokens: m.usage.input_tokens,
|
|
629
|
+
outputTokens: m.usage.output_tokens,
|
|
630
|
+
cacheCreationInputTokens: m.usage.cache_creation_input_tokens ?? undefined,
|
|
631
|
+
cacheReadInputTokens: m.usage.cache_read_input_tokens ?? undefined,
|
|
632
|
+
costUsd: m.total_cost_usd,
|
|
633
|
+
...(lastContextTokens !== undefined && contextWindow
|
|
634
|
+
? { contextTokens: lastContextTokens, contextWindow }
|
|
635
|
+
: {}),
|
|
636
|
+
}
|
|
637
|
+
: undefined,
|
|
638
|
+
};
|
|
639
|
+
yield* extractFileEvents(m.result, "result");
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
// Error result
|
|
643
|
+
const budgetExceeded = m.subtype === "error_max_budget_usd";
|
|
644
|
+
const errorMsg = budgetExceeded
|
|
645
|
+
? "The job reached its cost limit"
|
|
646
|
+
: m.errors?.join("; ") || "Unknown error";
|
|
647
|
+
yield {
|
|
648
|
+
type: "error",
|
|
649
|
+
content: errorMsg,
|
|
650
|
+
...(budgetExceeded ? { code: "budget_exceeded" as const } : {}),
|
|
651
|
+
...(m.usage
|
|
652
|
+
? {
|
|
653
|
+
usage: {
|
|
654
|
+
inputTokens: m.usage.input_tokens,
|
|
655
|
+
outputTokens: m.usage.output_tokens,
|
|
656
|
+
cacheCreationInputTokens: m.usage.cache_creation_input_tokens ?? undefined,
|
|
657
|
+
cacheReadInputTokens: m.usage.cache_read_input_tokens ?? undefined,
|
|
658
|
+
costUsd: m.total_cost_usd,
|
|
659
|
+
},
|
|
660
|
+
}
|
|
661
|
+
: {}),
|
|
662
|
+
};
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// --- Assistant message ---
|
|
667
|
+
if (message.type === "assistant") {
|
|
668
|
+
const m = message as SDKAssistantMessage;
|
|
669
|
+
const content = (m.message?.content ?? []) as ContentBlock[];
|
|
670
|
+
const usage = m.message?.usage;
|
|
671
|
+
if (usage) {
|
|
672
|
+
lastContextTokens =
|
|
673
|
+
(usage.input_tokens ?? 0) +
|
|
674
|
+
(usage.cache_creation_input_tokens ?? 0) +
|
|
675
|
+
(usage.cache_read_input_tokens ?? 0) +
|
|
676
|
+
(usage.output_tokens ?? 0);
|
|
677
|
+
lastContextModel = m.message.model;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
for (const block of content) {
|
|
681
|
+
if (block.type === "text") {
|
|
682
|
+
const textBlock = block as TextBlock;
|
|
683
|
+
yield { type: "text", content: textBlock.text };
|
|
684
|
+
// Surface mid-turn [FILE:/abs/path] tags right away, matching codex/
|
|
685
|
+
// maestro providers — without this, a model that narrates a file
|
|
686
|
+
// before its next tool call (rather than emitting the tag in the
|
|
687
|
+
// final `result` text) wouldn't trigger send-file at all. The file
|
|
688
|
+
// event handler dedups on normalized path so the same tag appearing
|
|
689
|
+
// again in `result` won't double-send.
|
|
690
|
+
yield* extractFileEvents(textBlock.text, "text");
|
|
691
|
+
} else if (block.type === "tool_use") {
|
|
692
|
+
const tb = block as ToolUseBlock;
|
|
693
|
+
yield {
|
|
694
|
+
type: "tool_use",
|
|
695
|
+
name: tb.name,
|
|
696
|
+
input: tb.input || {},
|
|
697
|
+
toolUseId: tb.id,
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// --- User message (tool results ride on user-role messages) ---
|
|
705
|
+
// The Anthropic message protocol puts tool_result blocks in USER
|
|
706
|
+
// messages, never assistant ones — without this branch the unified log
|
|
707
|
+
// records tool calls but no results, losing information codex topics
|
|
708
|
+
// keep (visible when bridging a claude topic to another agent).
|
|
709
|
+
if (message.type === "user") {
|
|
710
|
+
const um = message as SDKUserMessage | SDKUserMessageReplay;
|
|
711
|
+
// Replays re-deliver transcript history on resume; mapping them would
|
|
712
|
+
// duplicate every past tool_result into the unified log.
|
|
713
|
+
if ("isReplay" in um && um.isReplay) continue;
|
|
714
|
+
const content = um.message?.content;
|
|
715
|
+
if (!Array.isArray(content)) continue;
|
|
716
|
+
for (const block of content as ContentBlock[]) {
|
|
717
|
+
if (block.type !== "tool_result") continue;
|
|
718
|
+
const trBlock = block as ToolResultBlock;
|
|
719
|
+
const trContent =
|
|
720
|
+
typeof trBlock.content === "string" ? trBlock.content.slice(0, 200) : "";
|
|
721
|
+
yield {
|
|
722
|
+
type: "tool_result",
|
|
723
|
+
toolUseId: trBlock.tool_use_id || "",
|
|
724
|
+
content: trContent,
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
} catch (e) {
|
|
730
|
+
// Abort = user moved on; don't surface as an error message. The handler
|
|
731
|
+
// catches the abort signal separately via abortReason, so swallowing
|
|
732
|
+
// here matches both maestro and codex behavior.
|
|
733
|
+
if (isAbortError(e) || opts.abortController?.signal.aborted) return;
|
|
734
|
+
logger.error({ err: e }, "claudeProvider: SDK iteration failed");
|
|
735
|
+
yield { type: "error", content: errMsg(e) };
|
|
736
|
+
}
|
|
737
|
+
}
|