u-foo 1.9.7 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ufoo.js +5 -3
- package/package.json +2 -4
- package/src/agent/claudeEventTranslator.js +267 -0
- package/src/agent/claudeOauthTokenReader.js +52 -0
- package/src/agent/claudeThreadProvider.js +343 -0
- package/src/agent/cliRunner.js +10 -16
- package/src/agent/codexEventTranslator.js +78 -0
- package/src/agent/codexThreadProvider.js +181 -0
- package/src/agent/controllerToolExecutor.js +233 -0
- package/src/agent/credentials/claude.js +324 -0
- package/src/agent/credentials/codex.js +203 -0
- package/src/agent/credentials/index.js +106 -0
- package/src/agent/internalRunner.js +348 -3
- package/src/agent/loopObservability.js +190 -0
- package/src/agent/loopRuntime.js +457 -0
- package/src/agent/ptyRunner.js +8 -7
- package/src/agent/ufooAgent.js +178 -120
- package/src/agent/upstreamTransport.js +464 -0
- package/src/bus/utils.js +3 -2
- package/src/chat/dashboardView.js +51 -1
- package/src/chat/index.js +3 -1
- package/src/config.js +53 -17
- package/src/controller/flags.js +160 -0
- package/src/controller/gateRouter.js +201 -0
- package/src/controller/routerFastPath.js +22 -0
- package/src/controller/shadowGuard.js +280 -0
- package/src/daemon/index.js +2 -3
- package/src/daemon/promptLoop.js +33 -224
- package/src/daemon/promptRequest.js +360 -5
- package/src/daemon/status.js +2 -0
- package/src/history/inputTimeline.js +9 -4
- package/src/memory/index.js +24 -0
- package/src/providerapi/redactor.js +87 -0
- package/src/providerapi/shadowDiff.js +174 -0
- package/src/report/store.js +4 -3
- package/src/tools/handlers/ackBus.js +26 -0
- package/src/tools/handlers/common.js +64 -0
- package/src/tools/handlers/dispatchMessage.js +81 -0
- package/src/tools/handlers/listAgents.js +14 -0
- package/src/tools/handlers/readBusSummary.js +34 -0
- package/src/tools/handlers/readOpenDecisions.js +26 -0
- package/src/tools/handlers/readProjectRegistry.js +20 -0
- package/src/tools/handlers/readPromptHistory.js +123 -0
- package/src/tools/handlers/tier2.js +134 -0
- package/src/tools/index.js +55 -0
- package/src/tools/registry.js +69 -0
- package/src/tools/schemaFixtures.js +415 -0
- package/src/tools/tier0/listAgents.js +14 -0
- package/src/tools/tier0/readBusSummary.js +14 -0
- package/src/tools/tier0/readOpenDecisions.js +14 -0
- package/src/tools/tier0/readProjectRegistry.js +14 -0
- package/src/tools/tier0/readPromptHistory.js +14 -0
- package/src/tools/tier1/ackBus.js +14 -0
- package/src/tools/tier1/dispatchMessage.js +14 -0
- package/src/tools/tier1/routeAgent.js +14 -0
- package/src/tools/tier2/closeAgent.js +14 -0
- package/src/tools/tier2/launchAgent.js +14 -0
- package/src/tools/tier2/manageCron.js +14 -0
- package/src/tools/tier2/renameAgent.js +14 -0
- package/src/tools/types.js +75 -0
- package/src/tools/unimplemented.js +13 -0
- package/src/ufoo/paths.js +4 -0
- package/bin/ufoo-assistant-agent.js +0 -5
- package/bin/ufoo-engine.js +0 -25
- package/src/assistant/agent.js +0 -261
- package/src/assistant/bridge.js +0 -178
- package/src/assistant/constants.js +0 -15
- package/src/assistant/engine.js +0 -252
- package/src/assistant/stdio.js +0 -58
- package/src/assistant/ufooEngineCli.js +0 -312
package/bin/ufoo.js
CHANGED
|
@@ -29,13 +29,15 @@ async function main() {
|
|
|
29
29
|
}
|
|
30
30
|
if (cmd === "agent-runner") {
|
|
31
31
|
const agentType = argv[1] || "codex";
|
|
32
|
-
|
|
32
|
+
const extraArgs = argv.slice(2);
|
|
33
|
+
await runInternalRunner({ projectRoot: process.cwd(), agentType, extraArgs });
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
36
|
if (cmd === "agent-pty-runner") {
|
|
36
37
|
const agentType = argv[1] || "codex";
|
|
38
|
+
const extraArgs = argv.slice(2);
|
|
37
39
|
try {
|
|
38
|
-
await runPtyRunner({ projectRoot: process.cwd(), agentType });
|
|
40
|
+
await runPtyRunner({ projectRoot: process.cwd(), agentType, extraArgs });
|
|
39
41
|
} catch (err) {
|
|
40
42
|
const normalized = String(agentType || "").trim().toLowerCase();
|
|
41
43
|
if (normalized === "ufoo" || normalized === "ucode" || normalized === "ufoo-code") {
|
|
@@ -44,7 +46,7 @@ async function main() {
|
|
|
44
46
|
// Fallback to headless runner if PTY is unavailable
|
|
45
47
|
// eslint-disable-next-line no-console
|
|
46
48
|
console.error(`[pty-runner] ${err.message || err}. Falling back to headless internal runner.`);
|
|
47
|
-
await runInternalRunner({ projectRoot: process.cwd(), agentType });
|
|
49
|
+
await runInternalRunner({ projectRoot: process.cwd(), agentType, extraArgs });
|
|
48
50
|
}
|
|
49
51
|
return;
|
|
50
52
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "u-foo",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://ufoo.dev",
|
|
@@ -20,9 +20,7 @@
|
|
|
20
20
|
"uclaude": "bin/uclaude.js",
|
|
21
21
|
"ucodex": "bin/ucodex.js",
|
|
22
22
|
"ucode": "bin/ucode.js",
|
|
23
|
-
"ucode-core": "bin/ucode-core.js"
|
|
24
|
-
"ufoo-assistant-agent": "bin/ufoo-assistant-agent.js",
|
|
25
|
-
"ufoo-engine": "bin/ufoo-engine.js"
|
|
23
|
+
"ucode-core": "bin/ucode-core.js"
|
|
26
24
|
},
|
|
27
25
|
"files": [
|
|
28
26
|
"bin/",
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function toObject(value) {
|
|
4
|
+
return value && typeof value === "object" ? value : {};
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function toArray(value) {
|
|
8
|
+
return Array.isArray(value) ? value : [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function safeStringify(value) {
|
|
12
|
+
if (typeof value === "string") return value;
|
|
13
|
+
if (value === undefined) return "";
|
|
14
|
+
try {
|
|
15
|
+
return JSON.stringify(value);
|
|
16
|
+
} catch {
|
|
17
|
+
return "";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseJsonArgs(raw = "") {
|
|
22
|
+
const text = String(raw || "").trim();
|
|
23
|
+
if (!text) return {};
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(text);
|
|
26
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
27
|
+
} catch {
|
|
28
|
+
return { __raw: text };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function toNonNegativeInt(value) {
|
|
33
|
+
const num = Number(value);
|
|
34
|
+
if (!Number.isFinite(num) || num < 0) return 0;
|
|
35
|
+
return Math.floor(num);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeClaudeUsage(usage = null) {
|
|
39
|
+
if (!usage || typeof usage !== "object") return null;
|
|
40
|
+
const item = toObject(usage);
|
|
41
|
+
return {
|
|
42
|
+
...item,
|
|
43
|
+
input_tokens: toNonNegativeInt(item.input_tokens),
|
|
44
|
+
output_tokens: toNonNegativeInt(item.output_tokens),
|
|
45
|
+
cache_creation_tokens: toNonNegativeInt(
|
|
46
|
+
item.cache_creation_tokens
|
|
47
|
+
|| item.cache_creation_input_tokens
|
|
48
|
+
|| (item.cache_creation && item.cache_creation.input_tokens)
|
|
49
|
+
),
|
|
50
|
+
cache_read_tokens: toNonNegativeInt(
|
|
51
|
+
item.cache_read_tokens
|
|
52
|
+
|| item.cache_read_input_tokens
|
|
53
|
+
|| (item.cache_read && item.cache_read.input_tokens)
|
|
54
|
+
),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createClaudeEventState(seed = {}) {
|
|
59
|
+
return {
|
|
60
|
+
turnId: String(seed.turnId || ""),
|
|
61
|
+
threadId: String(seed.threadId || ""),
|
|
62
|
+
usage: null,
|
|
63
|
+
stopReason: "",
|
|
64
|
+
contentBlocks: new Map(),
|
|
65
|
+
assistantBlocks: [],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizeClaudeContentBlock(block = {}) {
|
|
70
|
+
const item = toObject(block);
|
|
71
|
+
const type = String(item.type || "").trim();
|
|
72
|
+
if (!type) return [];
|
|
73
|
+
|
|
74
|
+
if (type === "text") {
|
|
75
|
+
return [{
|
|
76
|
+
type: "text_delta",
|
|
77
|
+
delta: String(item.text || ""),
|
|
78
|
+
itemType: "text",
|
|
79
|
+
}];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (type === "tool_use") {
|
|
83
|
+
return [{
|
|
84
|
+
type: "tool_call",
|
|
85
|
+
toolCallId: String(item.id || item.tool_use_id || ""),
|
|
86
|
+
name: String(item.name || ""),
|
|
87
|
+
args: toObject(item.input),
|
|
88
|
+
}];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (type === "tool_result") {
|
|
92
|
+
return [{
|
|
93
|
+
type: "tool_result",
|
|
94
|
+
toolCallId: String(item.tool_use_id || item.id || ""),
|
|
95
|
+
output: Object.prototype.hasOwnProperty.call(item, "content") ? item.content : item.output,
|
|
96
|
+
is_error: item.is_error === true,
|
|
97
|
+
}];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeClaudeMessage(message = {}) {
|
|
104
|
+
const item = toObject(message);
|
|
105
|
+
const turnId = String(item.id || item.turn_id || "");
|
|
106
|
+
const usage = normalizeClaudeUsage(item.usage || null);
|
|
107
|
+
const events = [];
|
|
108
|
+
if (turnId) {
|
|
109
|
+
events.push({ type: "turn_started", turnId });
|
|
110
|
+
}
|
|
111
|
+
for (const block of toArray(item.content)) {
|
|
112
|
+
events.push(...normalizeClaudeContentBlock(block));
|
|
113
|
+
}
|
|
114
|
+
if (usage) {
|
|
115
|
+
events.push({ type: "usage", turnId, usage });
|
|
116
|
+
}
|
|
117
|
+
events.push({
|
|
118
|
+
type: "turn_completed",
|
|
119
|
+
turnId,
|
|
120
|
+
usage,
|
|
121
|
+
stopReason: String(item.stop_reason || ""),
|
|
122
|
+
});
|
|
123
|
+
return events;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function appendAssistantBlock(state, block) {
|
|
127
|
+
if (!block || typeof block !== "object") return;
|
|
128
|
+
state.assistantBlocks.push(block);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getContentBlockState(state, index) {
|
|
132
|
+
const key = Number.isFinite(Number(index)) ? Number(index) : -1;
|
|
133
|
+
if (!state.contentBlocks.has(key)) {
|
|
134
|
+
state.contentBlocks.set(key, {
|
|
135
|
+
type: "",
|
|
136
|
+
text: "",
|
|
137
|
+
toolCallId: "",
|
|
138
|
+
name: "",
|
|
139
|
+
jsonText: "",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return state.contentBlocks.get(key);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function normalizeClaudeEvent(event = {}, state = createClaudeEventState()) {
|
|
146
|
+
const item = toObject(event);
|
|
147
|
+
const type = String(item.type || "").trim();
|
|
148
|
+
if (!type) return [];
|
|
149
|
+
|
|
150
|
+
if (type === "message_start") {
|
|
151
|
+
const message = toObject(item.message);
|
|
152
|
+
state.turnId = String(message.id || state.turnId || "");
|
|
153
|
+
state.usage = normalizeClaudeUsage(message.usage || state.usage || null);
|
|
154
|
+
return [{
|
|
155
|
+
type: "turn_started",
|
|
156
|
+
turnId: state.turnId,
|
|
157
|
+
}];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (type === "content_block_start") {
|
|
161
|
+
const block = toObject(item.content_block);
|
|
162
|
+
const blockState = getContentBlockState(state, item.index);
|
|
163
|
+
blockState.type = String(block.type || "").trim();
|
|
164
|
+
if (blockState.type === "text") {
|
|
165
|
+
blockState.text = String(block.text || "");
|
|
166
|
+
if (blockState.text) {
|
|
167
|
+
appendAssistantBlock(state, { type: "text", text: blockState.text });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (blockState.type === "tool_use") {
|
|
171
|
+
blockState.toolCallId = String(block.id || block.tool_use_id || "");
|
|
172
|
+
blockState.name = String(block.name || "");
|
|
173
|
+
blockState.jsonText = safeStringify(block.input || "");
|
|
174
|
+
}
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (type === "content_block_delta") {
|
|
179
|
+
const delta = toObject(item.delta);
|
|
180
|
+
const deltaType = String(delta.type || "").trim();
|
|
181
|
+
const blockState = getContentBlockState(state, item.index);
|
|
182
|
+
if (deltaType === "text_delta") {
|
|
183
|
+
const text = String(delta.text || "");
|
|
184
|
+
if (!text) return [];
|
|
185
|
+
blockState.text += text;
|
|
186
|
+
if (state.assistantBlocks.length > 0) {
|
|
187
|
+
const last = state.assistantBlocks[state.assistantBlocks.length - 1];
|
|
188
|
+
if (last && last.type === "text") {
|
|
189
|
+
last.text = `${String(last.text || "")}${text}`;
|
|
190
|
+
} else {
|
|
191
|
+
appendAssistantBlock(state, { type: "text", text });
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
appendAssistantBlock(state, { type: "text", text });
|
|
195
|
+
}
|
|
196
|
+
return [{
|
|
197
|
+
type: "text_delta",
|
|
198
|
+
delta: text,
|
|
199
|
+
itemType: "text",
|
|
200
|
+
}];
|
|
201
|
+
}
|
|
202
|
+
if (deltaType === "input_json_delta") {
|
|
203
|
+
blockState.jsonText += String(delta.partial_json || "");
|
|
204
|
+
}
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (type === "content_block_stop") {
|
|
209
|
+
const blockState = getContentBlockState(state, item.index);
|
|
210
|
+
state.contentBlocks.delete(Number.isFinite(Number(item.index)) ? Number(item.index) : -1);
|
|
211
|
+
if (blockState.type !== "tool_use") return [];
|
|
212
|
+
const args = parseJsonArgs(blockState.jsonText);
|
|
213
|
+
const assistantBlock = {
|
|
214
|
+
type: "tool_use",
|
|
215
|
+
id: blockState.toolCallId,
|
|
216
|
+
name: blockState.name,
|
|
217
|
+
input: args,
|
|
218
|
+
};
|
|
219
|
+
appendAssistantBlock(state, assistantBlock);
|
|
220
|
+
return [{
|
|
221
|
+
type: "tool_call",
|
|
222
|
+
toolCallId: blockState.toolCallId,
|
|
223
|
+
name: blockState.name,
|
|
224
|
+
args,
|
|
225
|
+
}];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (type === "message_delta") {
|
|
229
|
+
const usage = normalizeClaudeUsage(item.usage || null);
|
|
230
|
+
if (usage) state.usage = usage;
|
|
231
|
+
const delta = toObject(item.delta);
|
|
232
|
+
if (delta.stop_reason) state.stopReason = String(delta.stop_reason || "");
|
|
233
|
+
return usage ? [{
|
|
234
|
+
type: "usage",
|
|
235
|
+
turnId: state.turnId,
|
|
236
|
+
usage,
|
|
237
|
+
}] : [];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (type === "message_stop") {
|
|
241
|
+
return [{
|
|
242
|
+
type: "turn_completed",
|
|
243
|
+
turnId: state.turnId,
|
|
244
|
+
usage: state.usage,
|
|
245
|
+
stopReason: state.stopReason,
|
|
246
|
+
}];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (type === "error") {
|
|
250
|
+
const error = toObject(item.error);
|
|
251
|
+
return [{
|
|
252
|
+
type: "turn_failed",
|
|
253
|
+
turnId: state.turnId,
|
|
254
|
+
error: String(error.message || item.message || "claude stream failed"),
|
|
255
|
+
}];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
module.exports = {
|
|
262
|
+
createClaudeEventState,
|
|
263
|
+
normalizeClaudeContentBlock,
|
|
264
|
+
normalizeClaudeEvent,
|
|
265
|
+
normalizeClaudeMessage,
|
|
266
|
+
normalizeClaudeUsage,
|
|
267
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
ClaudeUpstreamCredentialResolver,
|
|
5
|
+
DEFAULT_REFRESH_WINDOW_MS,
|
|
6
|
+
resolveClaudeOauthPaths,
|
|
7
|
+
parseClaudeOauthFile,
|
|
8
|
+
serializeClaudeOauthToken,
|
|
9
|
+
classifyTokenState,
|
|
10
|
+
withLockFile,
|
|
11
|
+
atomicWriteJson,
|
|
12
|
+
} = require("./credentials/claude");
|
|
13
|
+
const { toLegacyResolvedAuth } = require("./credentials");
|
|
14
|
+
|
|
15
|
+
class ClaudeOauthTokenReader {
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.resolver = new ClaudeUpstreamCredentialResolver(options);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
resolvePaths() {
|
|
21
|
+
return this.resolver.resolvePaths();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
readTokenFile() {
|
|
25
|
+
return this.resolver.readTokenFile();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
buildResolvedAuth(tokenRecord) {
|
|
29
|
+
return toLegacyResolvedAuth(this.resolver.buildResolvedCredential(tokenRecord));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async refreshIfNeeded(initialRecord) {
|
|
33
|
+
const descriptor = await this.resolver.refreshIfNeeded(initialRecord);
|
|
34
|
+
return toLegacyResolvedAuth(descriptor);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async resolveAuth() {
|
|
38
|
+
const descriptor = await this.resolver.resolveCredentials();
|
|
39
|
+
return toLegacyResolvedAuth(descriptor);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
ClaudeOauthTokenReader,
|
|
45
|
+
DEFAULT_REFRESH_WINDOW_MS,
|
|
46
|
+
resolveClaudeOauthPaths,
|
|
47
|
+
parseClaudeOauthFile,
|
|
48
|
+
serializeClaudeOauthToken,
|
|
49
|
+
classifyTokenState,
|
|
50
|
+
withLockFile,
|
|
51
|
+
atomicWriteJson,
|
|
52
|
+
};
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
createClaudeEventState,
|
|
5
|
+
normalizeClaudeEvent,
|
|
6
|
+
} = require("./claudeEventTranslator");
|
|
7
|
+
const { redactUfooEvent } = require("../providerapi/redactor");
|
|
8
|
+
const { sendUpstreamRequest } = require("./upstreamTransport");
|
|
9
|
+
|
|
10
|
+
const CACHE_CONTROL = Object.freeze({ type: "ephemeral" });
|
|
11
|
+
|
|
12
|
+
function createThreadId() {
|
|
13
|
+
return `claude-thread-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function resolveAnthropicSdk() {
|
|
17
|
+
try {
|
|
18
|
+
// Optional dependency during Phase 1b seam work.
|
|
19
|
+
// eslint-disable-next-line global-require, import/no-extraneous-dependencies
|
|
20
|
+
return require("@anthropic-ai/sdk");
|
|
21
|
+
} catch (err) {
|
|
22
|
+
const error = new Error("Claude API seam enabled but @anthropic-ai/sdk is not installed");
|
|
23
|
+
error.code = "ANTHROPIC_SDK_UNAVAILABLE";
|
|
24
|
+
error.cause = err;
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function defaultClaudeAuthProvider() {
|
|
30
|
+
const apiKey = String(process.env.ANTHROPIC_API_KEY || "").trim();
|
|
31
|
+
if (!apiKey) {
|
|
32
|
+
const error = new Error("Claude API seam requires an authProvider or ANTHROPIC_API_KEY");
|
|
33
|
+
error.code = "CLAUDE_AUTH_UNAVAILABLE";
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
return { apiKey };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function defaultClaudeClientFactory({ sdk, auth = {} }) {
|
|
40
|
+
if (auth.client) return auth.client;
|
|
41
|
+
const Anthropic = sdk && (sdk.Anthropic || sdk.default || sdk);
|
|
42
|
+
if (!Anthropic) {
|
|
43
|
+
throw new Error("Anthropic SDK seam missing Anthropic client constructor");
|
|
44
|
+
}
|
|
45
|
+
const options = {};
|
|
46
|
+
if (auth.apiKey) options.apiKey = auth.apiKey;
|
|
47
|
+
if (auth.baseUrl) options.baseURL = auth.baseUrl;
|
|
48
|
+
if (auth.headers && typeof auth.headers === "object") {
|
|
49
|
+
options.defaultHeaders = { ...auth.headers };
|
|
50
|
+
}
|
|
51
|
+
return new Anthropic(options);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function defaultClaudeStreamFactory({ client, request }) {
|
|
55
|
+
if (!client || !client.messages || typeof client.messages.create !== "function") {
|
|
56
|
+
throw new Error("Claude API seam requires client.messages.create");
|
|
57
|
+
}
|
|
58
|
+
return client.messages.create({
|
|
59
|
+
...request,
|
|
60
|
+
stream: true,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function* defaultClaudeTransportStreamFactory({ request, auth = {}, model = "", attempt = 0 }) {
|
|
65
|
+
const runtime = {
|
|
66
|
+
provider: "claude",
|
|
67
|
+
transport: "anthropic-messages",
|
|
68
|
+
model: String(model || request.model || "").trim(),
|
|
69
|
+
baseUrl: String(auth.baseUrl || process.env.ANTHROPIC_BASE_URL || "https://api.anthropic.com/v1").trim(),
|
|
70
|
+
auth,
|
|
71
|
+
credentialSource: auth.apiKey ? "thread-auth" : "thread-headers",
|
|
72
|
+
};
|
|
73
|
+
const result = await sendUpstreamRequest({
|
|
74
|
+
runtime,
|
|
75
|
+
request,
|
|
76
|
+
timeoutMs: Number.isFinite(Number(request.timeout_ms)) ? Number(request.timeout_ms) : 120000,
|
|
77
|
+
});
|
|
78
|
+
if (!result.ok) {
|
|
79
|
+
const err = new Error(result.error || "Claude upstream request failed");
|
|
80
|
+
err.code = "CLAUDE_UPSTREAM_FAILED";
|
|
81
|
+
err.attempt = attempt;
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
yield {
|
|
86
|
+
type: "message_start",
|
|
87
|
+
message: {
|
|
88
|
+
id: `msg-${Date.now().toString(36)}`,
|
|
89
|
+
usage: result.usage || undefined,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
yield {
|
|
93
|
+
type: "content_block_delta",
|
|
94
|
+
index: 0,
|
|
95
|
+
delta: {
|
|
96
|
+
type: "text_delta",
|
|
97
|
+
text: String(result.output || ""),
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
yield { type: "message_stop" };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeToolDefinition(tool = {}) {
|
|
104
|
+
const item = tool && typeof tool === "object" ? tool : {};
|
|
105
|
+
return {
|
|
106
|
+
name: String(item.name || "").trim(),
|
|
107
|
+
description: String(item.description || "").trim(),
|
|
108
|
+
input_schema: item.input_schema || item.inputSchema || { type: "object", properties: {} },
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function normalizeMessageInput(input) {
|
|
113
|
+
if (typeof input === "string") {
|
|
114
|
+
return {
|
|
115
|
+
role: "user",
|
|
116
|
+
content: [{ type: "text", text: input }],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (Array.isArray(input)) {
|
|
120
|
+
return {
|
|
121
|
+
role: "user",
|
|
122
|
+
content: input,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (input && typeof input === "object" && input.role && input.content) {
|
|
126
|
+
return {
|
|
127
|
+
role: String(input.role),
|
|
128
|
+
content: Array.isArray(input.content) ? input.content : [input.content],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
role: "user",
|
|
133
|
+
content: [{ type: "text", text: String(input || "") }],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function normalizeContentBlocks(content) {
|
|
138
|
+
if (Array.isArray(content)) return content.filter((item) => item && typeof item === "object");
|
|
139
|
+
if (content && typeof content === "object") return [content];
|
|
140
|
+
return [{ type: "text", text: String(content || "") }];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function withCacheControlOnLastBlock(content) {
|
|
144
|
+
const blocks = normalizeContentBlocks(content).map((block) => ({ ...block }));
|
|
145
|
+
if (blocks.length === 0) return blocks;
|
|
146
|
+
const lastIndex = blocks.length - 1;
|
|
147
|
+
blocks[lastIndex] = {
|
|
148
|
+
...blocks[lastIndex],
|
|
149
|
+
cache_control: { ...CACHE_CONTROL },
|
|
150
|
+
};
|
|
151
|
+
return blocks;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function buildClaudeSystemBlocks(opts = {}) {
|
|
155
|
+
const blocks = [];
|
|
156
|
+
const staticText = String(opts.staticText || opts.systemPrompt || opts.system || "").trim();
|
|
157
|
+
const semistaticText = String(opts.semistaticText || opts.sessionPrompt || "").trim();
|
|
158
|
+
const dynamicText = String(opts.dynamicText || "").trim();
|
|
159
|
+
|
|
160
|
+
if (staticText) {
|
|
161
|
+
blocks.push({
|
|
162
|
+
type: "text",
|
|
163
|
+
text: staticText,
|
|
164
|
+
cache_control: { ...CACHE_CONTROL },
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (semistaticText) {
|
|
168
|
+
blocks.push({
|
|
169
|
+
type: "text",
|
|
170
|
+
text: semistaticText,
|
|
171
|
+
cache_control: { ...CACHE_CONTROL },
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (dynamicText) {
|
|
175
|
+
blocks.push({
|
|
176
|
+
type: "text",
|
|
177
|
+
text: dynamicText,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return blocks;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function buildClaudeRequestMessages(history = [], userMessage) {
|
|
185
|
+
const prior = Array.isArray(history) ? history : [];
|
|
186
|
+
const messages = prior.map((message) => ({
|
|
187
|
+
role: String(message && message.role ? message.role : "user"),
|
|
188
|
+
content: withCacheControlOnLastBlock(message && message.content ? message.content : []),
|
|
189
|
+
}));
|
|
190
|
+
messages.push(userMessage);
|
|
191
|
+
return messages;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function buildClaudeRequest({
|
|
195
|
+
model,
|
|
196
|
+
maxTokens,
|
|
197
|
+
messages = [],
|
|
198
|
+
userMessage,
|
|
199
|
+
tools = [],
|
|
200
|
+
promptCache = {},
|
|
201
|
+
}) {
|
|
202
|
+
const request = {
|
|
203
|
+
model,
|
|
204
|
+
max_tokens: maxTokens,
|
|
205
|
+
messages: buildClaudeRequestMessages(messages, userMessage),
|
|
206
|
+
};
|
|
207
|
+
const system = buildClaudeSystemBlocks(promptCache);
|
|
208
|
+
if (system.length > 0) request.system = system;
|
|
209
|
+
if (tools.length > 0) request.tools = tools;
|
|
210
|
+
return request;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function createAssistantMessageFromState(state) {
|
|
214
|
+
return {
|
|
215
|
+
role: "assistant",
|
|
216
|
+
content: Array.isArray(state.assistantBlocks) ? state.assistantBlocks.slice() : [],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function shouldRetryClaudeStream(err, attempt) {
|
|
221
|
+
if (attempt >= 1) return false;
|
|
222
|
+
const code = String((err && err.code) || "").trim().toUpperCase();
|
|
223
|
+
if (code === "ABORT_ERR" || code === "ECONNRESET" || code === "ETIMEDOUT") return true;
|
|
224
|
+
const message = String((err && err.message) || "").toLowerCase();
|
|
225
|
+
return message.includes("stream") || message.includes("network") || message.includes("disconnect");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
class ClaudeApiThread {
|
|
229
|
+
constructor({
|
|
230
|
+
model = "",
|
|
231
|
+
authProvider = defaultClaudeAuthProvider,
|
|
232
|
+
clientFactory = defaultClaudeClientFactory,
|
|
233
|
+
streamFactory = defaultClaudeStreamFactory,
|
|
234
|
+
sdk,
|
|
235
|
+
maxTokens = 4096,
|
|
236
|
+
} = {}) {
|
|
237
|
+
this.id = "";
|
|
238
|
+
this.model = model;
|
|
239
|
+
this.authProvider = authProvider;
|
|
240
|
+
this.clientFactory = clientFactory;
|
|
241
|
+
this.streamFactory = streamFactory;
|
|
242
|
+
this.sdk = sdk;
|
|
243
|
+
this.maxTokens = maxTokens;
|
|
244
|
+
this.messages = [];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async *runStreamed(input, opts = {}) {
|
|
248
|
+
if (!this.id) this.id = createThreadId();
|
|
249
|
+
const userMessage = normalizeMessageInput(input);
|
|
250
|
+
const requestMessages = this.messages.slice();
|
|
251
|
+
const tools = Array.isArray(opts.tools) ? opts.tools.map(normalizeToolDefinition).filter((tool) => tool.name) : [];
|
|
252
|
+
const request = buildClaudeRequest({
|
|
253
|
+
model: this.model,
|
|
254
|
+
maxTokens: Number.isFinite(Number(opts.maxTokens)) ? Number(opts.maxTokens) : this.maxTokens,
|
|
255
|
+
messages: requestMessages,
|
|
256
|
+
userMessage,
|
|
257
|
+
tools,
|
|
258
|
+
promptCache: opts.promptCache || {},
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
yield redactUfooEvent({ type: "thread_started", threadId: this.id });
|
|
262
|
+
|
|
263
|
+
const auth = await this.authProvider({ threadId: this.id, model: this.model });
|
|
264
|
+
const client = this.clientFactory({
|
|
265
|
+
sdk: this.sdk || resolveAnthropicSdk(),
|
|
266
|
+
auth,
|
|
267
|
+
model: this.model,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
let lastError = null;
|
|
271
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
272
|
+
const state = createClaudeEventState({ threadId: this.id });
|
|
273
|
+
try {
|
|
274
|
+
const stream = await this.streamFactory({
|
|
275
|
+
client,
|
|
276
|
+
request,
|
|
277
|
+
auth,
|
|
278
|
+
attempt,
|
|
279
|
+
});
|
|
280
|
+
for await (const rawEvent of stream) {
|
|
281
|
+
const events = normalizeClaudeEvent(rawEvent, state);
|
|
282
|
+
for (const event of events) {
|
|
283
|
+
if (event && typeof event === "object") {
|
|
284
|
+
yield redactUfooEvent(event);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
this.messages = requestMessages.concat([userMessage, createAssistantMessageFromState(state)]);
|
|
289
|
+
return;
|
|
290
|
+
} catch (err) {
|
|
291
|
+
lastError = err;
|
|
292
|
+
if (!shouldRetryClaudeStream(err, attempt)) {
|
|
293
|
+
throw err;
|
|
294
|
+
}
|
|
295
|
+
// TODO(phase-1b-iii): if retry-after-partial-stream appears in practice,
|
|
296
|
+
// add replay/dedupe before claude runner cutover. This slice retries only once.
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
throw lastError || new Error("Claude stream failed");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async close() {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
class ClaudeThreadProvider {
|
|
309
|
+
constructor(options = {}) {
|
|
310
|
+
this.options = { ...options };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
startThread() {
|
|
314
|
+
return new ClaudeApiThread(this.options);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
resumeThread(threadId = "") {
|
|
318
|
+
const thread = this.startThread();
|
|
319
|
+
thread.id = String(threadId || "").trim();
|
|
320
|
+
return thread;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function createClaudeThreadProvider(options = {}) {
|
|
325
|
+
return new ClaudeThreadProvider(options);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
module.exports = {
|
|
329
|
+
buildClaudeRequest,
|
|
330
|
+
buildClaudeRequestMessages,
|
|
331
|
+
buildClaudeSystemBlocks,
|
|
332
|
+
ClaudeApiThread,
|
|
333
|
+
ClaudeThreadProvider,
|
|
334
|
+
createClaudeThreadProvider,
|
|
335
|
+
defaultClaudeAuthProvider,
|
|
336
|
+
defaultClaudeClientFactory,
|
|
337
|
+
defaultClaudeStreamFactory,
|
|
338
|
+
defaultClaudeTransportStreamFactory,
|
|
339
|
+
normalizeMessageInput,
|
|
340
|
+
normalizeToolDefinition,
|
|
341
|
+
resolveAnthropicSdk,
|
|
342
|
+
withCacheControlOnLastBlock,
|
|
343
|
+
};
|