@opengeni/runtime 4.6.0 → 4.7.0-canary.37090756480001
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/agent-instructions/modules/media.d.ts +3 -0
- package/dist/anthropic-messages.d.ts +13 -3
- package/dist/anthropic-request-error.d.ts +10 -0
- package/dist/assets/codemode-client.json +1 -1
- package/dist/{chunk-A22BKN4P.js → chunk-LJNCGUMB.js} +3 -3
- package/dist/chunk-LJNCGUMB.js.map +1 -0
- package/dist/{chunk-GTWKWJ67.js → chunk-P2OVGKDG.js} +505 -100
- package/dist/chunk-P2OVGKDG.js.map +1 -0
- package/dist/{chunk-D5DPVA2C.js → chunk-R25L27T3.js} +2047 -1275
- package/dist/chunk-R25L27T3.js.map +1 -0
- package/dist/claude-subscription-usage.d.ts +14 -4
- package/dist/context-compaction.d.ts +7 -7
- package/dist/index.d.ts +2 -0
- package/dist/index.js +13 -3
- package/dist/interaction-tools.d.ts +1 -1
- package/dist/mcp-network.js +1 -1
- package/dist/runtime-skills.d.ts +1 -0
- package/dist/sandbox/channel-a.d.ts +3 -0
- package/dist/sandbox/index.d.ts +3 -1
- package/dist/sandbox/index.js +7 -1
- package/dist/sandbox/provider-command-session.d.ts +21 -0
- package/dist/sandbox/providers/modal-command-argv.d.ts +4 -0
- package/dist/sandbox/providers/modal-command-control.d.ts +17 -5
- package/dist/sandbox/providers/modal-command-observation-errors.d.ts +3 -0
- package/dist/sandbox/providers/modal-command-raw-page.d.ts +100 -0
- package/dist/sandbox/providers/modal-command-router-wire.d.ts +17 -0
- package/dist/sandbox/routing/routing-session.d.ts +10 -0
- package/dist/sandbox/turn-tool-cancellation.d.ts +4 -0
- package/dist/workspace-tool-gateway.js +3 -3
- package/package.json +12 -12
- package/src/agent-instructions/PROMPT_CHANGELOG.md +23 -1
- package/src/agent-instructions/compose.ts +2 -0
- package/src/agent-instructions/modules/media.ts +20 -0
- package/src/agent-instructions/runtime-mechanics.ts +8 -0
- package/src/anthropic-messages.ts +223 -31
- package/src/anthropic-request-error.ts +45 -0
- package/src/bundled_default_skills/opengeni-client/SKILL.md +9 -7
- package/src/bundled_default_skills/opengeni-client/references/agent-recipes.md +1 -1
- package/src/bundled_default_skills/opengeni-client/references/api-workflows.md +61 -0
- package/src/bundled_default_skills/opengeni-client/references/compatibility-and-troubleshooting.md +86 -0
- package/src/bundled_default_skills/opengeni-client/references/data-tools-and-credentials.md +82 -0
- package/src/bundled_default_skills/opengeni-client/references/discovery-and-autonomy.md +32 -25
- package/src/bundled_default_skills/opengeni-client/references/external-users-and-connect.md +17 -0
- package/src/bundled_default_skills/opengeni-client/references/product-shapes-and-ui.md +156 -0
- package/src/bundled_schedule_skills/opengeni-schedules/SKILL.md +56 -0
- package/src/claude-subscription-usage.ts +55 -9
- package/src/context-compaction.ts +16 -9
- package/src/index.ts +85 -18
- package/src/interaction-tools.ts +113 -3
- package/src/lazy-tool-transport.ts +26 -10
- package/src/mcp-network.ts +4 -2
- package/src/model-provider-client.ts +11 -3
- package/src/model-provider-routing.ts +4 -0
- package/src/runtime-skills.ts +8 -0
- package/src/sandbox/channel-a.ts +3 -0
- package/src/sandbox/index.ts +11 -1
- package/src/sandbox/provider-command-session.ts +58 -0
- package/src/sandbox/provider-errors.ts +43 -9
- package/src/sandbox/providers/modal-command-argv.ts +20 -0
- package/src/sandbox/providers/modal-command-control.ts +422 -116
- package/src/sandbox/providers/modal-command-observation-errors.ts +65 -0
- package/src/sandbox/providers/modal-command-raw-page.ts +147 -0
- package/src/sandbox/providers/modal-command-router-wire.ts +104 -12
- package/src/sandbox/providers/modal-command-session.ts +26 -4
- package/src/sandbox/providers/modal-legacy-command-control.ts +2 -9
- package/src/sandbox/providers/modal-materialization-verification.ts +70 -6
- package/src/sandbox/routing/routing-session.ts +140 -41
- package/src/sandbox/run-credentials.ts +62 -19
- package/src/sandbox/turn-tool-cancellation.ts +159 -42
- package/dist/chunk-A22BKN4P.js.map +0 -1
- package/dist/chunk-D5DPVA2C.js.map +0 -1
- package/dist/chunk-GTWKWJ67.js.map +0 -1
|
@@ -43,7 +43,7 @@ Composition order (each part separated by a blank line):
|
|
|
43
43
|
waiting and `wait_for_input`, compaction, background commands.
|
|
44
44
|
- Conditional modules, in this order: `renderer_markdown`, `sandbox`,
|
|
45
45
|
`connected_machine`, `repositories`, `workspace_environment`, `rig`,
|
|
46
|
-
`artifacts`, `goals`, `subagents`, `knowledge`, `skills`, `admin`,
|
|
46
|
+
`artifacts`, `media`, `goals`, `subagents`, `knowledge`, `skills`, `admin`,
|
|
47
47
|
`attachments` (last because it varies per turn).
|
|
48
48
|
3. Attempt directives, unchanged text: Codemode, code search, Git credential
|
|
49
49
|
bindings.
|
|
@@ -82,6 +82,28 @@ Headings that moved or were added (layout only, not diffed):
|
|
|
82
82
|
|
|
83
83
|
## Sentence edits (modular "all" vs legacy)
|
|
84
84
|
|
|
85
|
+
Tool discovery and media. Deferred schemas must remain behind the existing
|
|
86
|
+
search router, while modular agents need to distinguish missing disclosure
|
|
87
|
+
from missing execution authority. Runtime mechanics explains ranked discovery
|
|
88
|
+
and literal-prefix recovery; the new `media` capability module directs image
|
|
89
|
+
and video requests to runtime/provider tools rather than integration setup.
|
|
90
|
+
Disabled media removes that module. The legacy prompt remains unchanged.
|
|
91
|
+
|
|
92
|
+
```diff
|
|
93
|
+
+ Deferred tool schemas are omitted from the first request; absence there does not prove a tool is unavailable.
|
|
94
|
+
+ When deferred tools are attached, use `tool_search` for one focused capability at a time; broad searches with small limits can omit a relevant tool.
|
|
95
|
+
+ If a search misses, use `tool_list` and follow `nextCursor` until the relevant authorized names are covered, then load exact names with `tool_search`.
|
|
96
|
+
+ `namePrefix` is a literal tool-name prefix, not a capability keyword; an empty filtered page does not prove the capability is unavailable.
|
|
97
|
+
+ Discovery never grants authority, and remembered tool names must still resolve against the current authorized catalog.
|
|
98
|
+
+ Use an attached image-generation tool when the user requests generated images or edits.
|
|
99
|
+
+ If it is deferred, use a focused image-generation query with `tool_search`, or request the exact `generate_image` name before concluding no image tool exists.
|
|
100
|
+
+ `generate_image` is a runtime tool; integration catalogs and sandbox CLI lists do not enumerate it.
|
|
101
|
+
+ A hosted `image_generation` tool may instead be visible directly on supported provider routes.
|
|
102
|
+
+ The media capability permits discovery, but does not prove a provider adapter is attached; establish availability from the current authorized tool catalog.
|
|
103
|
+
+ Use the returned tool schema for references and output controls; do not invent model names or provider-specific options.
|
|
104
|
+
+ For video, discover `get_video_generation_capabilities` and `generate_video`, read current capabilities before selecting a model, and follow the available video-generation Skill.
|
|
105
|
+
```
|
|
106
|
+
|
|
85
107
|
Identity. The legacy text opened with two identity statements (the contract's
|
|
86
108
|
generic line and the template's OpenGeni line). The default identity keeps the
|
|
87
109
|
more specific one and adds the contract's collaboration and voice sentences,
|
|
@@ -13,6 +13,7 @@ import { rigModule, workspaceEnvironmentModule } from "./modules/environment";
|
|
|
13
13
|
import { goalsModule } from "./modules/goals";
|
|
14
14
|
import { knowledgeModule } from "./modules/knowledge";
|
|
15
15
|
import { connectedMachineModule } from "./modules/machines";
|
|
16
|
+
import { mediaModule } from "./modules/media";
|
|
16
17
|
import { rendererMarkdownModule } from "./modules/renderer-markdown";
|
|
17
18
|
import { repositoriesModule } from "./modules/repositories";
|
|
18
19
|
import { sandboxModule } from "./modules/sandbox";
|
|
@@ -33,6 +34,7 @@ export const AGENT_PROMPT_MODULES: readonly AgentPromptModule[] = [
|
|
|
33
34
|
workspaceEnvironmentModule,
|
|
34
35
|
rigModule,
|
|
35
36
|
artifactsModule,
|
|
37
|
+
mediaModule,
|
|
36
38
|
goalsModule,
|
|
37
39
|
subagentsModule,
|
|
38
40
|
knowledgeModule,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { blocks, sentences, type AgentPromptModule } from "../types";
|
|
2
|
+
|
|
3
|
+
/** Media authorization is separate from provider attachment and tool disclosure. */
|
|
4
|
+
export const mediaModule: AgentPromptModule = {
|
|
5
|
+
id: "media",
|
|
6
|
+
applies: (context) => context.capabilities.media,
|
|
7
|
+
render: () =>
|
|
8
|
+
blocks(
|
|
9
|
+
"# Images and video",
|
|
10
|
+
sentences(
|
|
11
|
+
"Use an attached image-generation tool when the user requests generated images or edits.",
|
|
12
|
+
"If it is deferred, use a focused image-generation query with `tool_search`, or request the exact `generate_image` name before concluding no image tool exists.",
|
|
13
|
+
"`generate_image` is a runtime tool; integration catalogs and sandbox CLI lists do not enumerate it.",
|
|
14
|
+
"A hosted `image_generation` tool may instead be visible directly on supported provider routes.",
|
|
15
|
+
"The media capability permits discovery, but does not prove a provider adapter is attached; establish availability from the current authorized tool catalog.",
|
|
16
|
+
"Use the returned tool schema for references and output controls; do not invent model names or provider-specific options.",
|
|
17
|
+
"For video, discover `get_video_generation_capabilities` and `generate_video`, read current capabilities before selecting a model, and follow the available video-generation Skill.",
|
|
18
|
+
),
|
|
19
|
+
),
|
|
20
|
+
};
|
|
@@ -43,6 +43,14 @@ Command completion alone resumes you only while you have an explicit \`wait_for_
|
|
|
43
43
|
export function renderRuntimeMechanics(context: AgentPromptContext): string {
|
|
44
44
|
return blocks(
|
|
45
45
|
"# Runtime mechanics",
|
|
46
|
+
"## Tool discovery",
|
|
47
|
+
sentences(
|
|
48
|
+
"Deferred tool schemas are omitted from the first request; absence there does not prove a tool is unavailable.",
|
|
49
|
+
"When deferred tools are attached, use `tool_search` for one focused capability at a time; broad searches with small limits can omit a relevant tool.",
|
|
50
|
+
"If a search misses, use `tool_list` and follow `nextCursor` until the relevant authorized names are covered, then load exact names with `tool_search`.",
|
|
51
|
+
"`namePrefix` is a literal tool-name prefix, not a capability keyword; an empty filtered page does not prove the capability is unavailable.",
|
|
52
|
+
"Discovery never grants authority, and remembered tool names must still resolve against the current authorized catalog.",
|
|
53
|
+
),
|
|
46
54
|
"## New messages while you work",
|
|
47
55
|
newMessages(context),
|
|
48
56
|
"## Waiting",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { applyClaudeCodeIdentity } from "./claude-code-identity";
|
|
3
|
+
import { AnthropicRequestError } from "./anthropic-request-error";
|
|
3
4
|
import {
|
|
4
5
|
protocol,
|
|
5
6
|
Usage,
|
|
@@ -8,7 +9,8 @@ import {
|
|
|
8
9
|
type ModelResponse,
|
|
9
10
|
type ResponseStreamEvent,
|
|
10
11
|
} from "@openai/agents";
|
|
11
|
-
import type
|
|
12
|
+
import { claudeNativeModelProfile, type ResolvedModelProvider } from "@opengeni/config";
|
|
13
|
+
import { withClaudeModelRequest } from "./claude-subscription-usage";
|
|
12
14
|
|
|
13
15
|
type Json = Record<string, any>;
|
|
14
16
|
type Message = { role: "user" | "assistant" | "system"; content: Json[] };
|
|
@@ -17,6 +19,75 @@ export class AnthropicProtocolError extends Error {
|
|
|
17
19
|
readonly code = "anthropic_protocol_error";
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
/** Closed provider rejections carry authored copy, never arbitrary response text. */
|
|
23
|
+
export class AnthropicProviderRejection extends Error {
|
|
24
|
+
readonly name = "AnthropicProviderRejection";
|
|
25
|
+
constructor(
|
|
26
|
+
readonly code:
|
|
27
|
+
| "anthropic_model_access_suspended"
|
|
28
|
+
| "anthropic_permission_denied"
|
|
29
|
+
| "content_policy_violation",
|
|
30
|
+
readonly status: number,
|
|
31
|
+
readonly request_id?: string,
|
|
32
|
+
readonly suspendedUntil?: string,
|
|
33
|
+
readonly headers: Record<string, string> = {},
|
|
34
|
+
) {
|
|
35
|
+
super(
|
|
36
|
+
code === "anthropic_model_access_suspended"
|
|
37
|
+
? suspendedUntil
|
|
38
|
+
? `Claude suspended access to this model for the connected account until ${suspendedUntil.replace("T", " ").replace(/(?:\.000)?Z$/, " UTC")}. Try again after that time.`
|
|
39
|
+
: "Claude suspended access to this model for the connected account. Signing in again will not lift this restriction."
|
|
40
|
+
: code === "content_policy_violation"
|
|
41
|
+
? "Claude blocked this request through its safety systems. Automatic retries stopped."
|
|
42
|
+
: "Claude denied this request (HTTP 403). Check the connected account's permissions.",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function suspensionDeadline(error: Json): string | undefined {
|
|
48
|
+
if (typeof error.message !== "string") return undefined;
|
|
49
|
+
const raw =
|
|
50
|
+
/^model: "[A-Za-z0-9._:/-]{1,128}" is suspended for this organization until (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z)$/.exec(
|
|
51
|
+
error.message,
|
|
52
|
+
)?.[1];
|
|
53
|
+
if (!raw) return undefined;
|
|
54
|
+
const date = new Date(raw);
|
|
55
|
+
if (!Number.isFinite(date.getTime())) return undefined;
|
|
56
|
+
const normalized = date.toISOString();
|
|
57
|
+
return normalized.replace(/\.000Z$/, "Z") === raw.replace(/\.000Z$/, "Z")
|
|
58
|
+
? normalized
|
|
59
|
+
: undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function providerRejection(
|
|
63
|
+
status: number,
|
|
64
|
+
detail: unknown,
|
|
65
|
+
responseHeaders: Headers,
|
|
66
|
+
): AnthropicProviderRejection | undefined {
|
|
67
|
+
if (status !== 403) return undefined;
|
|
68
|
+
const requestId = responseHeaders.get("request-id") ?? undefined;
|
|
69
|
+
const headers = responseHeaders.has("retry-after")
|
|
70
|
+
? { "retry-after": responseHeaders.get("retry-after")! }
|
|
71
|
+
: {};
|
|
72
|
+
const error =
|
|
73
|
+
detail && typeof detail === "object" && !Array.isArray(detail) ? (detail as Json) : {};
|
|
74
|
+
if (error.type === "permission_error" && error.details?.error_code === "model_access_suspended")
|
|
75
|
+
return new AnthropicProviderRejection(
|
|
76
|
+
"anthropic_model_access_suspended",
|
|
77
|
+
status,
|
|
78
|
+
requestId,
|
|
79
|
+
suspensionDeadline(error),
|
|
80
|
+
headers,
|
|
81
|
+
);
|
|
82
|
+
return new AnthropicProviderRejection(
|
|
83
|
+
"anthropic_permission_denied",
|
|
84
|
+
status,
|
|
85
|
+
requestId,
|
|
86
|
+
undefined,
|
|
87
|
+
headers,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
20
91
|
function object(value: unknown): Json {
|
|
21
92
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
22
93
|
throw new AnthropicProtocolError("Expected an object in the Claude protocol");
|
|
@@ -232,10 +303,51 @@ export function anthropicMessages(input: ModelRequest["input"]): Message[] {
|
|
|
232
303
|
return messages;
|
|
233
304
|
}
|
|
234
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Compaction retains user/system inputs but removes their intervening assistant
|
|
308
|
+
* replies. Anthropic's system beta requires a system message after a user and
|
|
309
|
+
* before an assistant (or at the end), not user -> system -> user. Coalesce each
|
|
310
|
+
* user phase and its system blocks at that boundary, retaining their own roles,
|
|
311
|
+
* exact content and relative order. A machine-only continuation after an
|
|
312
|
+
* assistant needs a request-local input anchor; it is not a new human message.
|
|
313
|
+
* Never move a system across an assistant.
|
|
314
|
+
*/
|
|
315
|
+
function placeConversationSystems(messages: Message[]): Message[] {
|
|
316
|
+
const result: Message[] = [];
|
|
317
|
+
let systems: Json[] = [];
|
|
318
|
+
const flush = () => {
|
|
319
|
+
if (!systems.length) return;
|
|
320
|
+
if (result.at(-1)?.role !== "user")
|
|
321
|
+
result.push({
|
|
322
|
+
role: "user",
|
|
323
|
+
content: [
|
|
324
|
+
{
|
|
325
|
+
type: "text",
|
|
326
|
+
text: "OpenGeni continuation (machine-origin input; no new human message).",
|
|
327
|
+
},
|
|
328
|
+
],
|
|
329
|
+
});
|
|
330
|
+
result.push({ role: "system", content: systems });
|
|
331
|
+
systems = [];
|
|
332
|
+
};
|
|
333
|
+
for (const message of messages) {
|
|
334
|
+
if (message.role === "system") {
|
|
335
|
+
systems.push(...message.content);
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (message.role === "assistant") flush();
|
|
339
|
+
const previous = result.at(-1);
|
|
340
|
+
if (previous?.role === message.role) previous.content.push(...message.content);
|
|
341
|
+
else result.push(message);
|
|
342
|
+
}
|
|
343
|
+
flush();
|
|
344
|
+
return result;
|
|
345
|
+
}
|
|
346
|
+
|
|
235
347
|
export function buildAnthropicRequest(
|
|
236
348
|
request: ModelRequest,
|
|
237
349
|
model: string,
|
|
238
|
-
provider: Pick<ResolvedModelProvider, "anthropic"
|
|
350
|
+
provider: Pick<ResolvedModelProvider, "anthropic"> & Partial<Pick<ResolvedModelProvider, "kind">>,
|
|
239
351
|
stream: boolean,
|
|
240
352
|
): Json {
|
|
241
353
|
if (request.previousResponseId || request.conversationId)
|
|
@@ -245,6 +357,13 @@ export function buildAnthropicRequest(
|
|
|
245
357
|
if (request.prompt)
|
|
246
358
|
throw new AnthropicProtocolError("OpenAI prompt templates cannot be used with Claude");
|
|
247
359
|
const settings = request.modelSettings;
|
|
360
|
+
const profile = claudeNativeModelProfile(model);
|
|
361
|
+
const managed =
|
|
362
|
+
provider.kind === "anthropic-workspace" ||
|
|
363
|
+
provider.kind === "anthropic-organization" ||
|
|
364
|
+
provider.kind === "claude-subscription-workspace" ||
|
|
365
|
+
provider.kind === "claude-subscription-organization";
|
|
366
|
+
const outputLimit = profile?.maxOutputTokens ?? (managed ? 32_000 : undefined);
|
|
248
367
|
const names = toolNames(request);
|
|
249
368
|
const forcedName = (name: string): string => {
|
|
250
369
|
const matches = [...names].filter(([, identity]) => identity.name === name);
|
|
@@ -254,16 +373,16 @@ export function buildAnthropicRequest(
|
|
|
254
373
|
);
|
|
255
374
|
return matches[0]![0];
|
|
256
375
|
};
|
|
257
|
-
|
|
376
|
+
let messages = anthropicMessages(request.input);
|
|
258
377
|
if (!messages.length) throw new AnthropicProtocolError("Claude requires at least one message");
|
|
259
378
|
const system = request.systemInstructions
|
|
260
379
|
? ([{ type: "text", text: request.systemInstructions }] as Json[])
|
|
261
380
|
: [];
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
// messages at their original history position.
|
|
381
|
+
// Initial system/developer instructions belong in the top-level field. Later
|
|
382
|
+
// systems keep their authority within the same assistant-delimited phase.
|
|
265
383
|
while (messages[0]?.role === "system") system.push(...messages.shift()!.content);
|
|
266
384
|
if (!messages.length) throw new AnthropicProtocolError("Claude requires a conversation message");
|
|
385
|
+
messages = placeConversationSystems(messages);
|
|
267
386
|
const tools: Json[] = request.tools.map((tool) => {
|
|
268
387
|
if (tool.type !== "function")
|
|
269
388
|
throw new AnthropicProtocolError(`Claude does not support the ${tool.type} tool transport`);
|
|
@@ -308,7 +427,13 @@ export function buildAnthropicRequest(
|
|
|
308
427
|
}
|
|
309
428
|
const body: Json = {
|
|
310
429
|
model,
|
|
311
|
-
max_tokens:
|
|
430
|
+
max_tokens: Math.min(
|
|
431
|
+
settings.maxTokens ??
|
|
432
|
+
provider.anthropic?.maxOutputTokens ??
|
|
433
|
+
profile?.maxOutputTokens ??
|
|
434
|
+
32000,
|
|
435
|
+
outputLimit ?? Infinity,
|
|
436
|
+
),
|
|
312
437
|
messages,
|
|
313
438
|
stream,
|
|
314
439
|
};
|
|
@@ -326,13 +451,20 @@ export function buildAnthropicRequest(
|
|
|
326
451
|
...(body.tool_choice ?? { type: "auto" }),
|
|
327
452
|
disable_parallel_tool_use: true,
|
|
328
453
|
};
|
|
329
|
-
const
|
|
454
|
+
const requestedEffort = settings.reasoning?.effort;
|
|
455
|
+
// Preserve actual native levels; do not silently turn Extra into High.
|
|
456
|
+
const effort = requestedEffort === "minimal" ? "low" : requestedEffort;
|
|
457
|
+
const supportsThinking = profile ? profile.efforts.length > 0 : !managed;
|
|
330
458
|
// Anthropic forbids forced tool selection together with thinking.
|
|
331
459
|
const forcedTool = body.tool_choice?.type === "any" || body.tool_choice?.type === "tool";
|
|
332
|
-
if (effort && effort !== "none" && !forcedTool) {
|
|
460
|
+
if (supportsThinking && effort && effort !== "none" && !forcedTool) {
|
|
461
|
+
if (profile && !profile.efforts.includes(effort))
|
|
462
|
+
throw new AnthropicProtocolError(
|
|
463
|
+
"The selected Claude model does not support this reasoning effort",
|
|
464
|
+
);
|
|
333
465
|
body.thinking = { type: "adaptive" };
|
|
334
466
|
body.output_config = {
|
|
335
|
-
effort
|
|
467
|
+
effort,
|
|
336
468
|
};
|
|
337
469
|
} else if (settings.temperature !== undefined) body.temperature = settings.temperature;
|
|
338
470
|
if (settings.topP !== undefined && !body.thinking) body.top_p = settings.topP;
|
|
@@ -375,11 +507,11 @@ export function anthropicResponse(
|
|
|
375
507
|
requestId?: string,
|
|
376
508
|
names = new Map<string, { name: string; namespace?: string }>(),
|
|
377
509
|
): ModelResponse {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
)
|
|
382
|
-
)
|
|
510
|
+
// A refused response can be HTTP 200 with no content blocks. It must not
|
|
511
|
+
// become an empty successful response or admit a tool call from partial output.
|
|
512
|
+
if (message.stop_reason === "refusal")
|
|
513
|
+
throw new AnthropicProviderRejection("content_policy_violation", 200, requestId);
|
|
514
|
+
if (!["end_turn", "tool_use", "stop_sequence", "max_tokens"].includes(message.stop_reason))
|
|
383
515
|
throw new AnthropicProtocolError(`Claude response did not finish: ${message.stop_reason}`);
|
|
384
516
|
text(message.id, "message ID");
|
|
385
517
|
if (!Array.isArray(message.content))
|
|
@@ -461,6 +593,7 @@ export class AnthropicMessagesModel implements Model {
|
|
|
461
593
|
) {}
|
|
462
594
|
|
|
463
595
|
private async send(request: ModelRequest, stream: boolean): Promise<Response> {
|
|
596
|
+
request.signal?.throwIfAborted();
|
|
464
597
|
const body = buildAnthropicRequest(request, this.model, this.provider, stream);
|
|
465
598
|
const base = this.provider.baseUrl ?? "https://api.anthropic.com/v1";
|
|
466
599
|
const url = new URL(`${base.replace(/\/$/, "")}/messages`);
|
|
@@ -470,6 +603,11 @@ export class AnthropicMessagesModel implements Model {
|
|
|
470
603
|
headers.set("content-type", "application/json");
|
|
471
604
|
headers.set("accept", stream ? "text/event-stream" : "application/json");
|
|
472
605
|
headers.set("anthropic-version", "2023-06-01");
|
|
606
|
+
if ((claudeNativeModelProfile(this.model)?.contextWindowTokens ?? 0) > 200_000) {
|
|
607
|
+
const betas = new Set((headers.get("anthropic-beta") ?? "").split(",").filter(Boolean));
|
|
608
|
+
betas.add("context-1m-2025-08-07");
|
|
609
|
+
headers.set("anthropic-beta", [...betas].join(","));
|
|
610
|
+
}
|
|
473
611
|
if (body.messages.some((message: Message) => message.role === "system")) {
|
|
474
612
|
const betas = new Set((headers.get("anthropic-beta") ?? "").split(",").filter(Boolean));
|
|
475
613
|
betas.add("mid-conversation-system-2026-04-07");
|
|
@@ -506,12 +644,18 @@ export class AnthropicMessagesModel implements Model {
|
|
|
506
644
|
previousRequestId: this.previousRequestId,
|
|
507
645
|
});
|
|
508
646
|
}
|
|
509
|
-
const response = await this.
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
647
|
+
const response = await withClaudeModelRequest(this.model, () =>
|
|
648
|
+
this.fetch(url, {
|
|
649
|
+
method: "POST",
|
|
650
|
+
headers,
|
|
651
|
+
body: JSON.stringify(body),
|
|
652
|
+
...(request.signal ? { signal: request.signal } : {}),
|
|
653
|
+
}),
|
|
654
|
+
);
|
|
655
|
+
if (request.signal?.aborted) {
|
|
656
|
+
void response.body?.cancel().catch(() => undefined);
|
|
657
|
+
request.signal.throwIfAborted();
|
|
658
|
+
}
|
|
515
659
|
if (!response.ok) {
|
|
516
660
|
const detail = await readErrorDetail(
|
|
517
661
|
response.body,
|
|
@@ -521,20 +665,33 @@ export class AnthropicMessagesModel implements Model {
|
|
|
521
665
|
const contextExceeded =
|
|
522
666
|
response.status === 400 &&
|
|
523
667
|
/prompt is too long|context_length_exceeded|exceeds.*context window/i.test(detail);
|
|
668
|
+
let providerError: unknown;
|
|
669
|
+
try {
|
|
670
|
+
providerError = JSON.parse(detail).error;
|
|
671
|
+
} catch {
|
|
672
|
+
/* Diagnostics can be truncated. */
|
|
673
|
+
}
|
|
674
|
+
const rejection = providerRejection(response.status, providerError, response.headers);
|
|
675
|
+
if (rejection) throw rejection;
|
|
524
676
|
// Classify the bounded provider detail without leaking echoed prompts or credentials.
|
|
525
677
|
const message = contextExceeded
|
|
526
678
|
? "Claude context window exceeded"
|
|
527
679
|
: response.status === 401
|
|
528
680
|
? "Claude credentials expired or were revoked. Replace the key or setup token in Models."
|
|
529
681
|
: "Claude request failed (HTTP " + response.status + ")";
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
682
|
+
let source: unknown;
|
|
683
|
+
try {
|
|
684
|
+
source = JSON.parse(detail)?.error;
|
|
685
|
+
} catch {
|
|
686
|
+
// Malformed, truncated or non-JSON bodies retain structural status only.
|
|
687
|
+
}
|
|
688
|
+
throw new AnthropicRequestError(
|
|
689
|
+
message,
|
|
690
|
+
response.status,
|
|
691
|
+
contextExceeded ? "context_length_exceeded" : "anthropic_http_error",
|
|
692
|
+
source,
|
|
693
|
+
response.headers,
|
|
694
|
+
);
|
|
538
695
|
}
|
|
539
696
|
this.previousRequestId = response.headers.get("request-id") ?? undefined;
|
|
540
697
|
return response;
|
|
@@ -558,6 +715,7 @@ export class AnthropicMessagesModel implements Model {
|
|
|
558
715
|
for await (const event of anthropicSse(
|
|
559
716
|
response.body,
|
|
560
717
|
this.provider.anthropic?.streamIdleTimeoutMs ?? 600000,
|
|
718
|
+
request.signal,
|
|
561
719
|
)) {
|
|
562
720
|
request.signal?.throwIfAborted();
|
|
563
721
|
if (event.type === "error") {
|
|
@@ -574,6 +732,8 @@ export class AnthropicMessagesModel implements Model {
|
|
|
574
732
|
overloaded_error: 529,
|
|
575
733
|
};
|
|
576
734
|
const status = typeof kind === "string" ? (statuses[kind] ?? 502) : 502;
|
|
735
|
+
const rejection = providerRejection(status, event.error, response.headers);
|
|
736
|
+
if (rejection) throw rejection;
|
|
577
737
|
throw Object.assign(new Error(`Claude stream failed (HTTP ${status})`), {
|
|
578
738
|
status,
|
|
579
739
|
code: status === 429 ? "rate_limit_exceeded" : "anthropic_stream_error",
|
|
@@ -581,6 +741,15 @@ export class AnthropicMessagesModel implements Model {
|
|
|
581
741
|
headers: response.headers.has("retry-after")
|
|
582
742
|
? { "retry-after": response.headers.get("retry-after")! }
|
|
583
743
|
: {},
|
|
744
|
+
// Keep the structural stream wrapper stable for provider rejection
|
|
745
|
+
// guards while the typed cause keeps provider text private.
|
|
746
|
+
cause: new AnthropicRequestError(
|
|
747
|
+
`Claude stream failed (HTTP ${status})`,
|
|
748
|
+
status,
|
|
749
|
+
status === 429 ? "rate_limit_exceeded" : "anthropic_stream_error",
|
|
750
|
+
event.error,
|
|
751
|
+
response.headers,
|
|
752
|
+
),
|
|
584
753
|
});
|
|
585
754
|
}
|
|
586
755
|
switch (event.type) {
|
|
@@ -641,6 +810,12 @@ export class AnthropicMessagesModel implements Model {
|
|
|
641
810
|
if (!message) throw new AnthropicProtocolError("Claude message_delta without a message");
|
|
642
811
|
Object.assign(message, event.delta);
|
|
643
812
|
message.usage = { ...message.usage, ...event.usage };
|
|
813
|
+
if (message.stop_reason === "refusal")
|
|
814
|
+
throw new AnthropicProviderRejection(
|
|
815
|
+
"content_policy_violation",
|
|
816
|
+
200,
|
|
817
|
+
response.headers.get("request-id") ?? undefined,
|
|
818
|
+
);
|
|
644
819
|
finalDelta = true;
|
|
645
820
|
break;
|
|
646
821
|
case "message_stop": {
|
|
@@ -670,23 +845,39 @@ export class AnthropicMessagesModel implements Model {
|
|
|
670
845
|
export async function* anthropicSse(
|
|
671
846
|
body: ReadableStream<Uint8Array>,
|
|
672
847
|
idleTimeoutMs = 600000,
|
|
848
|
+
signal?: AbortSignal,
|
|
673
849
|
): AsyncGenerator<Json> {
|
|
850
|
+
if (signal?.aborted) void body.cancel().catch(() => undefined);
|
|
851
|
+
signal?.throwIfAborted();
|
|
674
852
|
const reader = body.getReader();
|
|
675
853
|
const decoder = new TextDecoder();
|
|
676
854
|
let buffer = "";
|
|
677
855
|
let data: string[] = [];
|
|
678
856
|
try {
|
|
679
857
|
while (true) {
|
|
858
|
+
signal?.throwIfAborted();
|
|
680
859
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
860
|
+
let onAbort: (() => void) | undefined;
|
|
861
|
+
const aborted = new Promise<never>((_, reject) => {
|
|
862
|
+
if (signal) {
|
|
863
|
+
onAbort = () => reject(signal.reason);
|
|
864
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
865
|
+
if (signal.aborted) onAbort();
|
|
866
|
+
}
|
|
867
|
+
});
|
|
681
868
|
const chunk = await Promise.race([
|
|
682
869
|
reader.read(),
|
|
870
|
+
aborted,
|
|
683
871
|
new Promise<never>((_, reject) => {
|
|
684
872
|
timer = setTimeout(
|
|
685
873
|
() => reject(new AnthropicProtocolError("Claude stream timed out waiting for data")),
|
|
686
874
|
idleTimeoutMs,
|
|
687
875
|
);
|
|
688
876
|
}),
|
|
689
|
-
]).finally(() =>
|
|
877
|
+
]).finally(() => {
|
|
878
|
+
clearTimeout(timer);
|
|
879
|
+
if (signal && onAbort) signal.removeEventListener("abort", onAbort);
|
|
880
|
+
});
|
|
690
881
|
buffer += decoder.decode(chunk.value, { stream: !chunk.done });
|
|
691
882
|
let newline: number;
|
|
692
883
|
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
@@ -700,7 +891,8 @@ export async function* anthropicSse(
|
|
|
700
891
|
if (chunk.done) break;
|
|
701
892
|
}
|
|
702
893
|
} finally {
|
|
703
|
-
|
|
894
|
+
// A terminal rejection must not wait on untrusted transport cleanup.
|
|
895
|
+
void reader.cancel().catch(() => undefined);
|
|
704
896
|
reader.releaseLock();
|
|
705
897
|
}
|
|
706
898
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
|
|
3
|
+
function bounded(value: unknown, maxBytes: number): string | undefined {
|
|
4
|
+
if (typeof value !== "string" || !value) return undefined;
|
|
5
|
+
const bytes = Buffer.from(value);
|
|
6
|
+
if (bytes.length <= maxBytes) return value;
|
|
7
|
+
const marker = "… [truncated]";
|
|
8
|
+
let end = maxBytes - Buffer.byteLength(marker);
|
|
9
|
+
while (end > 0 && (bytes[end]! & 0xc0) === 0x80) end -= 1;
|
|
10
|
+
return bytes.subarray(0, end).toString("utf8") + marker;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Structural log message; only the durable failure payload reads provider detail. */
|
|
14
|
+
export class AnthropicRequestError extends Error {
|
|
15
|
+
readonly request_id: string | null;
|
|
16
|
+
readonly headers: Record<string, string>;
|
|
17
|
+
#detail: string | undefined;
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
message: string,
|
|
21
|
+
readonly status: number,
|
|
22
|
+
readonly code: string,
|
|
23
|
+
source: unknown,
|
|
24
|
+
headers: Headers,
|
|
25
|
+
) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "AnthropicRequestError";
|
|
28
|
+
const error =
|
|
29
|
+
source && typeof source === "object" && !Array.isArray(source)
|
|
30
|
+
? (source as Record<string, unknown>)
|
|
31
|
+
: undefined;
|
|
32
|
+
const type = bounded(error?.type, 256);
|
|
33
|
+
const detail = bounded(error?.message, 4096);
|
|
34
|
+
// Select only error.type/message, never the raw body, echoed request fields,
|
|
35
|
+
// cookies, or outgoing content. Keep diagnostics out of generic serialization.
|
|
36
|
+
this.#detail = bounded([type, detail].filter(Boolean).join(": "), 4096);
|
|
37
|
+
this.request_id = bounded(headers.get("request-id"), 256) ?? null;
|
|
38
|
+
const retryAfter = bounded(headers.get("retry-after"), 256);
|
|
39
|
+
this.headers = retryAfter ? { "retry-after": retryAfter } : {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get detail(): string | undefined {
|
|
43
|
+
return this.#detail;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -63,13 +63,15 @@ Embedded products may narrow bundled guidance with `bundledSkillIds`.
|
|
|
63
63
|
Continue useful discovery without requesting broad credentials or pretending
|
|
64
64
|
missing access is configured. Read
|
|
65
65
|
[Discovery and autonomy](references/discovery-and-autonomy.md) for that workflow.
|
|
66
|
-
-
|
|
67
|
-
or isolated)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
- Two choices belong to the user: who can see a chat (`chats`: private, shared
|
|
67
|
+
or isolated) and whether the agent may change data. If the request or
|
|
68
|
+
repository does not settle one, ask ONE short, plain-language question with a
|
|
69
|
+
recommended answer, before building. Never ask what the repository answers.
|
|
70
|
+
- Do not ask about background work, schedules, session length or credential
|
|
71
|
+
lifetime up front. Long agent sessions just work. Only when the requested
|
|
72
|
+
feature itself is scheduled or runs in the background (for example "email me
|
|
73
|
+
a weekly report"), ask for the missing details (day, time, time zone, where
|
|
74
|
+
results appear) and say in one sentence why you need them. See
|
|
73
75
|
[Discovery and autonomy](references/discovery-and-autonomy.md).
|
|
74
76
|
- Use a reversible, clearly stated default only for choices outside those four,
|
|
75
77
|
or when the user explicitly said not to ask. A busy user is not that signal.
|
|
@@ -77,7 +77,7 @@ session a short-lived per-user bearer and rotate it on every message:
|
|
|
77
77
|
never send credential updates itself.
|
|
78
78
|
4. The MCP server validates the token and enforces the user's own permissions.
|
|
79
79
|
|
|
80
|
-
Make the token outlive
|
|
80
|
+
Make the token outlive long agent work by default (hours, refreshed on every message); this is a default, never a question for the user. Header
|
|
81
81
|
rotation cannot change the server's URL or tools. Scheduled tasks cannot carry
|
|
82
82
|
inline `mcpServers`; background agents use a workspace MCP connection or
|
|
83
83
|
OpenAPI Integration instead.
|
|
@@ -203,6 +203,67 @@ Beyond `initialMessage`/`tools`/`resources`, the create body (`POST /v1/workspac
|
|
|
203
203
|
- Session creation exposes a workspace-scoped `idempotencyKey` (distinct from the per-call `clientEventId`): forward a stable value so concurrent/retried creates of the same logical session collapse to a single session. Without it every create is independent, so a blind retry can double-create — keep sending a stable key when you retry.
|
|
204
204
|
- Treat unknown event types as extensible timeline entries, not client crashes.
|
|
205
205
|
|
|
206
|
+
### Read a settled result without caching progress or losing text
|
|
207
|
+
|
|
208
|
+
`agent.message.completed` means one message ended, not that the turn completed.
|
|
209
|
+
`phase: "commentary"` is progress; even `final_answer` text precedes canonical
|
|
210
|
+
settlement. For a text-result cache, wait for the expected logical turn's
|
|
211
|
+
`turn.completed` and use its original `payload.output`. Empty output or
|
|
212
|
+
`emptyFinalReply` is not a successful report; a wait-ended turn may contain
|
|
213
|
+
`reply` for human attention without a result. Handle failure, cancellation,
|
|
214
|
+
approval and human-input states separately. Do not mark the whole session/goal
|
|
215
|
+
complete merely because one turn settled.
|
|
216
|
+
|
|
217
|
+
REST `listEvents` defaults to a bounded monitoring/summary projection. A compact
|
|
218
|
+
latest result is also a projection and can truncate long text. For an exact
|
|
219
|
+
saved result, use bounded `listEventPage` reads of original events, check
|
|
220
|
+
`forensicExact`, and follow the returned cursor, not a guessed sequence:
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
async function readOriginalResultPage(client, workspaceId, sessionId, after) {
|
|
224
|
+
const page = await client.listEventPage(workspaceId, sessionId, {
|
|
225
|
+
mode: "forensic", payloadMode: "full", direction: "after", after, limit: 50,
|
|
226
|
+
includeTypes: ["agent.message.completed", "turn.completed", "turn.failed",
|
|
227
|
+
"turn.cancelled", "session.requiresAction", "session.humanInput.requested"],
|
|
228
|
+
});
|
|
229
|
+
if (!page.forensicExact ||
|
|
230
|
+
(page.hasMore && (!Number.isSafeInteger(page.nextAfter) || page.nextAfter <= after))) {
|
|
231
|
+
throw new Error("An exact result page is not available.");
|
|
232
|
+
}
|
|
233
|
+
return page; // truncated/hasMore may mean more exact events, not truncated text
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function settledTurnResult(event, expectedTurnId) {
|
|
237
|
+
if (event.turnId !== expectedTurnId ||
|
|
238
|
+
["late_rejected", "duplicate"].includes(event.turnAssociation)) return null;
|
|
239
|
+
if (event.type === "turn.failed" || event.type === "turn.cancelled") {
|
|
240
|
+
return { state: event.type === "turn.failed" ? "failed" : "cancelled", text: null };
|
|
241
|
+
}
|
|
242
|
+
if (event.type !== "turn.completed") return null;
|
|
243
|
+
const payload = event.payload ?? {};
|
|
244
|
+
if (payload.emptyFinalReply === true || typeof payload.output !== "string" || !payload.output.trim()) {
|
|
245
|
+
return { state: "settled_without_result", text: null };
|
|
246
|
+
}
|
|
247
|
+
return { state: "completed", text: payload.output };
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Keep the newest candidate plus its original event ID/sequence while following
|
|
252
|
+
`hasMore`; publish/cache only after reaching the current page boundary. Preserve
|
|
253
|
+
the original full text rather than stripping a summary's truncation marker.
|
|
254
|
+
If exact retrieval is unavailable, report that explicitly; do not cache a
|
|
255
|
+
snippet as the final answer. Render only authorized result fields, never raw
|
|
256
|
+
forensic payloads. The existing UI/facade projections are preferable when they
|
|
257
|
+
already fit; this is a narrow backend text-result example, not a new timeline.
|
|
258
|
+
|
|
259
|
+
Use a background job or nonblocking bounded status ticks when a turn's tools
|
|
260
|
+
call back into the same host. Holding every scarce synchronous request worker
|
|
261
|
+
in a blocking wait can starve those callbacks under concurrency; it is a
|
|
262
|
+
deployment-dependent risk, not an inevitable failure. Bound each read/deadline,
|
|
263
|
+
retain callback capacity, and resume from the saved cursor. For static pages,
|
|
264
|
+
initialize enhancement scripts after their DOM targets exist (`defer`, modules
|
|
265
|
+
or `DOMContentLoaded`); retain the native form/POST fallback.
|
|
266
|
+
|
|
206
267
|
## Files
|
|
207
268
|
|
|
208
269
|
The usual flow is:
|