@schlenkr/ragents 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/apps/server/src/chat-handler.ts +2 -0
- package/apps/server/src/config.ts +6 -0
- package/apps/server/src/host-api.json +1 -0
- package/apps/server/src/layout.ts +0 -15
- package/apps/server/src/plugin-support/agent-tool.ts +2 -3
- package/apps/server/src/plugin-support/plugin-folder.ts +2 -1
- package/apps/server/src/provider.ts +34 -11
- package/apps/server/src/ragents/actor-chat-history.ts +1 -0
- package/apps/server/src/ragents/engine.ts +23 -24
- package/apps/server/src/ragents/journal-chat-events.ts +2 -0
- package/apps/server/src/ragents/run-owner.ts +3 -2
- package/apps/server/src/ragents/session.ts +3 -3
- package/apps/server/src/run-preparation.ts +14 -18
- package/apps/server/src/run-transfer.ts +65 -4
- package/apps/server/src/settings.ts +2 -4
- package/apps/web/dist/assets/{highlighted-body-KPVGNVTW-B9qYBARS.js → highlighted-body-KPVGNVTW-xXXhTDbh.js} +1 -1
- package/apps/web/dist/assets/{main-BCjvD2Hh.js → main-C7JeZFjs.js} +1 -1
- package/apps/web/dist/assets/{mermaid-HWGCJPDP-CvYYFK_n.js → mermaid-HWGCJPDP-DfsU5ofC.js} +69 -69
- package/apps/web/dist/assets/{runPanel-CG7kRMJh.js → runPanel-ZT6TPDLM.js} +1 -1
- package/apps/web/dist/help/guide-clients.html +13 -4
- package/apps/web/dist/help/guide-clients.md +14 -4
- package/apps/web/dist/help/guide-distributed.html +8 -3
- package/apps/web/dist/help/guide-distributed.md +8 -3
- package/apps/web/dist/help/guide-runtime.html +18 -12
- package/apps/web/dist/help/guide-runtime.md +19 -12
- package/apps/web/dist/help/mini-app.css +23 -3
- package/apps/web/dist/host-web.json +4 -4
- package/apps/web/dist/index.html +2 -2
- package/apps/web/dist/run-panel.html +2 -2
- package/apps/web/src/SessionList.tsx +16 -8
- package/apps/web/src/StartTiles.tsx +1 -1
- package/apps/web/src/api.ts +2 -0
- package/apps/web/src/panel/PanelPage.tsx +1 -1
- package/apps/web/src/panel/RunLine.tsx +4 -2
- package/apps/web/src/panel/contract.ts +2 -0
- package/bundles/ragents.activity/ragents-bundle.json +1 -1
- package/bundles/ragents.actor-programs/ragents-bundle.json +1 -1
- package/bundles/ragents.ask/ragents-bundle.json +1 -1
- package/bundles/ragents.browser/ragents-bundle.json +1 -1
- package/bundles/ragents.documents/ragents-bundle.json +1 -1
- package/bundles/ragents.lsp-fsharp/ragents-bundle.json +1 -1
- package/bundles/ragents.lsp-roslyn/ragents-bundle.json +1 -1
- package/bundles/ragents.lsp-typescript/ragents-bundle.json +1 -1
- package/bundles/ragents.model-relay/ragents-bundle.json +1 -1
- package/bundles/ragents.orchestration/ragents-bundle.json +1 -1
- package/bundles/ragents.overseer/ragents-bundle.json +1 -1
- package/bundles/ragents.processes/ragents-bundle.json +1 -1
- package/bundles/ragents.product/ragents-bundle.json +1 -1
- package/bundles/ragents.profile-distribution/ragents-bundle.json +1 -1
- package/bundles/ragents.reference/ragents-bundle.json +2 -2
- package/bundles/ragents.reference/run-scripts/learning-afternoon/tests/program.test.ts +1 -1
- package/bundles/ragents.reference/run-scripts/word-game/tests/program.test.ts +3 -3
- package/bundles/ragents.todo/ragents-bundle.json +1 -1
- package/bundles/ragents.transcript/ragents-bundle.json +1 -1
- package/bundles/ragents.watch/ragents-bundle.json +1 -1
- package/bundles/ragents.workspace/ragents-bundle.json +1 -1
- package/package.json +2 -2
- package/packages/agent/dist/core/agent-settings.d.ts +2 -2
- package/packages/agent/dist/core/compaction/compaction.d.ts +16 -50
- package/packages/agent/dist/core/context-log.d.ts +25 -0
- package/packages/agent/dist/core/messages.d.ts +3 -10
- package/packages/agent/dist/core/tool-definition.d.ts +17 -0
- package/packages/agent/dist/core/tools/bash.d.ts +2 -7
- package/packages/agent/dist/core/tools/edit.d.ts +1 -1
- package/packages/agent/dist/core/tools/read.d.ts +1 -1
- package/packages/agent/dist/core/tools/write.d.ts +1 -1
- package/packages/agent/dist/index.d.ts +7 -8
- package/packages/agent/dist/loop/agent.d.ts +2 -6
- package/packages/agent/package.json +1 -1
- package/packages/agent/src/core/agent-settings.ts +2 -2
- package/packages/agent/src/core/compaction/compaction.ts +49 -175
- package/packages/agent/src/core/context-log.ts +49 -0
- package/packages/agent/src/core/messages.ts +5 -11
- package/packages/agent/src/core/tool-definition.ts +29 -0
- package/packages/agent/src/core/tools/bash.ts +2 -9
- package/packages/agent/src/core/tools/edit.ts +2 -11
- package/packages/agent/src/core/tools/read.ts +3 -18
- package/packages/agent/src/core/tools/write.ts +1 -5
- package/packages/agent/src/index.ts +28 -14
- package/packages/agent/src/loop/agent-loop.ts +1 -2
- package/packages/agent/src/loop/agent.ts +8 -2
- package/packages/ai/dist/api/ai-sdk.d.ts +2 -0
- package/packages/ai/dist/types.d.ts +2 -0
- package/packages/ai/src/api/ai-sdk-messages.ts +15 -10
- package/packages/ai/src/api/ai-sdk.ts +11 -0
- package/packages/ai/src/api/transform-messages.ts +1 -1
- package/packages/ai/src/types.ts +2 -0
- package/packages/ai/src/utils/retry.ts +14 -0
- package/packages/ragents/src/agents/actor-input.ts +15 -0
- package/packages/ragents/src/agents/model-context.ts +360 -0
- package/packages/ragents/src/agents/plugins.ts +5 -0
- package/packages/ragents/src/agents/scheduler.ts +3 -7
- package/packages/ragents/src/agents/tools.ts +2 -2
- package/packages/ragents/src/agents/toolset.ts +47 -20
- package/packages/ragents/src/agents/workspaces.ts +0 -6
- package/packages/ragents/src/domain/event-semantics.ts +51 -0
- package/packages/ragents/src/domain/event-validation.ts +166 -0
- package/packages/ragents/src/domain/events.ts +72 -0
- package/packages/ragents/src/domain/model.ts +4 -2
- package/packages/ragents/src/domain/projection.ts +9 -0
- package/packages/ragents/src/domain/tool-result-text.ts +9 -0
- package/packages/ragents/src/drivers/agent-hooks.ts +25 -38
- package/packages/ragents/src/drivers/agent-runtime.ts +118 -933
- package/packages/ragents/src/drivers/agent-turn.ts +735 -0
- package/packages/ragents/src/drivers/agent.ts +9 -7
- package/packages/ragents/src/drivers/skill-preload.ts +53 -63
- package/packages/ragents/src/drivers/types.ts +21 -5
- package/packages/ragents/src/index.ts +8 -6
- package/packages/ragents/src/plugin-host.ts +60 -13
- package/packages/ragents/src/plugin-types.ts +7 -2
- package/packages/ragents/src/runtime/decisions/actors.ts +10 -0
- package/packages/ragents/src/runtime/decisions/turns.ts +66 -7
- package/packages/ragents/src/runtime/journal-storage.ts +7 -4
- package/packages/ragents/src/runtime/journal.ts +35 -0
- package/packages/ragents/src/runtime/orchestration.ts +140 -9
- package/packages/workspace-executor/src/sandbox-tools.ts +10 -11
- package/plugins/ragents.reference/run-scripts/learning-afternoon/tests/program.test.ts +1 -1
- package/plugins/ragents.reference/run-scripts/word-game/tests/program.test.ts +3 -3
- package/scripts/agent/journal.ts +5 -3
- package/scripts/package/publish-package.ts +4 -3
- package/packages/agent/dist/core/agent-session-runtime.d.ts +0 -33
- package/packages/agent/dist/core/agent-session-services.d.ts +0 -35
- package/packages/agent/dist/core/agent-session.d.ts +0 -234
- package/packages/agent/dist/core/auth-guidance.d.ts +0 -4
- package/packages/agent/dist/core/defaults.d.ts +0 -3
- package/packages/agent/dist/core/extensions/index.d.ts +0 -11
- package/packages/agent/dist/core/extensions/loader.d.ts +0 -12
- package/packages/agent/dist/core/extensions/runner.d.ts +0 -39
- package/packages/agent/dist/core/extensions/types.d.ts +0 -156
- package/packages/agent/dist/core/extensions/wrapper.d.ts +0 -12
- package/packages/agent/dist/core/resource-loader.d.ts +0 -39
- package/packages/agent/dist/core/sdk.d.ts +0 -38
- package/packages/agent/dist/core/session-manager.d.ts +0 -189
- package/packages/agent/dist/core/source-info.d.ts +0 -16
- package/packages/agent/dist/core/system-prompt.d.ts +0 -21
- package/packages/agent/src/core/agent-session-runtime.ts +0 -94
- package/packages/agent/src/core/agent-session-services.ts +0 -74
- package/packages/agent/src/core/agent-session.ts +0 -1108
- package/packages/agent/src/core/auth-guidance.ts +0 -14
- package/packages/agent/src/core/defaults.ts +0 -3
- package/packages/agent/src/core/extensions/index.ts +0 -38
- package/packages/agent/src/core/extensions/loader.ts +0 -125
- package/packages/agent/src/core/extensions/runner.ts +0 -211
- package/packages/agent/src/core/extensions/types.ts +0 -197
- package/packages/agent/src/core/extensions/wrapper.ts +0 -24
- package/packages/agent/src/core/resource-loader.ts +0 -142
- package/packages/agent/src/core/sdk.ts +0 -123
- package/packages/agent/src/core/session-manager.ts +0 -743
- package/packages/agent/src/core/source-info.ts +0 -28
- package/packages/agent/src/core/system-prompt.ts +0 -108
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
// Everything the engine, the server and the workspace executor use; the rest is internal.
|
|
2
|
-
export type { AgentSession } from "./core/agent-session.ts";
|
|
3
|
-
export type { ExtensionAPI, ExtensionContext, ExtensionError, InlineExtension, ToolDefinition } from "./core/extensions/index.ts";
|
|
4
|
-
export { defineTool } from "./core/extensions/index.ts";
|
|
5
2
|
export { aliasedModel, type ModelAlias, ModelRuntime } from "./core/model-runtime.ts";
|
|
6
|
-
export {
|
|
3
|
+
export { type AgentSettings, type AgentSettingsInput, agentSettings } from "./core/agent-settings.ts";
|
|
7
4
|
export {
|
|
8
|
-
|
|
9
|
-
type
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
5
|
+
activeContextEntries,
|
|
6
|
+
type ContextCompaction,
|
|
7
|
+
type ContextLogEntry,
|
|
8
|
+
contextMessages,
|
|
9
|
+
latestCompaction,
|
|
10
|
+
} from "./core/context-log.ts";
|
|
11
|
+
export {
|
|
12
|
+
calculateContextTokens,
|
|
13
|
+
compact,
|
|
14
|
+
estimateContextTokens,
|
|
15
|
+
prepareCompaction,
|
|
16
|
+
shouldCompact,
|
|
17
|
+
} from "./core/compaction/index.ts";
|
|
18
|
+
export { convertToLlm } from "./core/messages.ts";
|
|
19
|
+
export { formatSkillsForPrompt, type Skill } from "./core/skills.ts";
|
|
20
|
+
export { defineTool, type ToolDefinition } from "./core/tool-definition.ts";
|
|
18
21
|
export {
|
|
19
22
|
type BashOperations,
|
|
20
23
|
createBashToolDefinition,
|
|
@@ -22,5 +25,16 @@ export {
|
|
|
22
25
|
createReadToolDefinition,
|
|
23
26
|
createWriteToolDefinition,
|
|
24
27
|
} from "./core/tools/index.ts";
|
|
28
|
+
export {
|
|
29
|
+
Agent,
|
|
30
|
+
type AgentEvent,
|
|
31
|
+
type AgentLoopTurnUpdate,
|
|
32
|
+
type AgentMessage,
|
|
33
|
+
type AgentTool,
|
|
34
|
+
type AgentToolResult,
|
|
35
|
+
EMPTY_RESPONSE_NUDGE,
|
|
36
|
+
isRunFailure,
|
|
37
|
+
type ThinkingLevel,
|
|
38
|
+
} from "./loop/index.ts";
|
|
25
39
|
export { parseFrontmatter, stripFrontmatter } from "./utils/frontmatter.ts";
|
|
26
40
|
export { getShellConfig, killProcessTree, type ShellConfig } from "./utils/shell.ts";
|
|
@@ -715,8 +715,7 @@ function createToolResultMessage(finalized: FinalizedToolCallOutcome): ToolResul
|
|
|
715
715
|
role: "toolResult",
|
|
716
716
|
toolCallId: finalized.toolCall.id,
|
|
717
717
|
toolName: finalized.toolCall.name,
|
|
718
|
-
// Untyped tools
|
|
719
|
-
// so the null never enters session history or provider payloads.
|
|
718
|
+
// Untyped tools can return results without content; the null never enters the context.
|
|
720
719
|
content: finalized.result.content ?? [],
|
|
721
720
|
details: finalized.result.details,
|
|
722
721
|
...(finalized.result.addedToolNames?.length ? { addedToolNames: finalized.result.addedToolNames } : {}),
|
|
@@ -123,6 +123,11 @@ type ActiveRun = {
|
|
|
123
123
|
* `Agent` owns the current transcript, emits lifecycle events, executes tools,
|
|
124
124
|
* and polls its steering source before each model request.
|
|
125
125
|
*/
|
|
126
|
+
const runFailures = new WeakSet<object>();
|
|
127
|
+
|
|
128
|
+
/** Whether a message is the stand-in the agent reports for a failed run; no model produced it. */
|
|
129
|
+
export const isRunFailure = (message: AgentMessage): boolean => runFailures.has(message);
|
|
130
|
+
|
|
126
131
|
export class Agent {
|
|
127
132
|
private _state: MutableAgentState;
|
|
128
133
|
private readonly listeners = new Set<(event: AgentEvent, signal: AbortSignal) => Promise<void> | void>();
|
|
@@ -363,7 +368,7 @@ export class Agent {
|
|
|
363
368
|
}
|
|
364
369
|
|
|
365
370
|
private async handleRunFailure(error: unknown, aborted: boolean): Promise<void> {
|
|
366
|
-
const failureMessage = {
|
|
371
|
+
const failureMessage: AgentMessage = {
|
|
367
372
|
role: "assistant",
|
|
368
373
|
content: [{ type: "text", text: "" }],
|
|
369
374
|
api: this._state.model.api,
|
|
@@ -373,7 +378,8 @@ export class Agent {
|
|
|
373
378
|
stopReason: aborted ? "aborted" : "error",
|
|
374
379
|
errorMessage: error instanceof Error ? error.message : String(error),
|
|
375
380
|
timestamp: Date.now(),
|
|
376
|
-
}
|
|
381
|
+
};
|
|
382
|
+
runFailures.add(failureMessage);
|
|
377
383
|
await this.processEvents({ type: "message_start", message: failureMessage });
|
|
378
384
|
await this.processEvents({ type: "message_end", message: failureMessage });
|
|
379
385
|
await this.processEvents({ type: "turn_end", message: failureMessage, toolResults: [] });
|
|
@@ -9,5 +9,7 @@ export interface OpenRouterOptions extends StreamOptions {
|
|
|
9
9
|
reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
10
10
|
}
|
|
11
11
|
export declare const stream: StreamFunction<"openai-completions", OpenRouterOptions>;
|
|
12
|
+
/** Counts cache_control blocks anywhere in the request body, as the provider counts them. */
|
|
13
|
+
export declare const cacheMarksOf: (value: unknown) => number;
|
|
12
14
|
export declare const streamSimple: StreamFunction<"openai-completions", SimpleStreamOptions>;
|
|
13
15
|
//# sourceMappingURL=ai-sdk.d.ts.map
|
|
@@ -211,6 +211,8 @@ export interface UserMessage {
|
|
|
211
211
|
role: "user";
|
|
212
212
|
content: string | UserContent[];
|
|
213
213
|
timestamp: number;
|
|
214
|
+
/** A note for this one request only; the cache boundary stays in front of it. */
|
|
215
|
+
transient?: true;
|
|
214
216
|
}
|
|
215
217
|
export interface AssistantMessage {
|
|
216
218
|
role: "assistant";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AssistantModelMessage, FilePart, JSONValue, ModelMessage, UserContent as SdkUserContent } from "ai";
|
|
1
|
+
import type { AssistantModelMessage, FilePart, JSONValue, ModelMessage, ToolResultPart, UserContent as SdkUserContent } from "ai";
|
|
2
2
|
import type { Api, AssistantMessage, Context, Model, UserContent } from "../types.ts";
|
|
3
3
|
import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts";
|
|
4
4
|
import { transformMessages } from "./transform-messages.ts";
|
|
@@ -54,21 +54,33 @@ function assistantMessage(message: AssistantMessage): AssistantModelMessage | un
|
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/** The breakpoint sits on the last lasting block; on a part, because the SDK merges tool messages and copies a message mark onto every result. */
|
|
58
|
+
function markCacheBoundary(message: ModelMessage | undefined, cacheControl: CacheControl): void {
|
|
59
|
+
if (!message || message.role === "system") return;
|
|
60
|
+
const lastPart = message.role === "tool" ? message.content.filter((part): part is ToolResultPart => part.type === "tool-result").at(-1)
|
|
61
|
+
: message.role === "user" && Array.isArray(message.content) ? message.content.at(-1) : undefined;
|
|
62
|
+
const target = lastPart ?? message;
|
|
63
|
+
target.providerOptions = { ...target.providerOptions, openrouter: { ...target.providerOptions?.openrouter, cacheControl } };
|
|
64
|
+
}
|
|
65
|
+
|
|
57
66
|
export function convertMessages(context: Context, model: Model<Api>, cacheControl?: CacheControl): ModelMessage[] {
|
|
58
67
|
const result: ModelMessage[] = [];
|
|
59
68
|
if (context.systemPrompt) result.push({ role: "system", content: sanitizeSurrogates(context.systemPrompt),
|
|
60
69
|
...(cacheControl ? { providerOptions: { openrouter: { cacheControl } } } : {}),
|
|
61
70
|
});
|
|
62
71
|
const messages = transformMessages(context.messages, model);
|
|
72
|
+
let boundary: ModelMessage | undefined;
|
|
63
73
|
for (let index = 0; index < messages.length; index += 1) {
|
|
64
74
|
const message = messages[index]!;
|
|
65
75
|
if (message.role === "user") {
|
|
66
76
|
const content = typeof message.content === "string" ? sanitizeSurrogates(message.content)
|
|
67
77
|
: message.content.filter((part) => part.type !== "text" || part.text.length > 0).map(userContent);
|
|
68
78
|
if (typeof content === "string" || content.length > 0) result.push({ role: "user", content });
|
|
79
|
+
if (!message.transient) boundary = result.at(-1);
|
|
69
80
|
} else if (message.role === "assistant") {
|
|
70
81
|
const converted = assistantMessage(message);
|
|
71
82
|
if (converted) result.push(converted);
|
|
83
|
+
boundary = result.at(-1);
|
|
72
84
|
} else {
|
|
73
85
|
const images: FilePart[] = [];
|
|
74
86
|
while (messages[index]?.role === "toolResult") {
|
|
@@ -87,16 +99,9 @@ export function convertMessages(context: Context, model: Model<Api>, cacheContro
|
|
|
87
99
|
}
|
|
88
100
|
index -= 1;
|
|
89
101
|
if (images.length > 0) result.push({ role: "user", content: [{ type: "text", text: "Attached image(s) from tool result:" }, ...images] });
|
|
102
|
+
boundary = result.at(-1);
|
|
90
103
|
}
|
|
91
104
|
}
|
|
92
|
-
if (cacheControl)
|
|
93
|
-
const lastMessage = [...result].reverse().find((message) => (message.role === "user" || message.role === "assistant")
|
|
94
|
-
&& (typeof message.content === "string" ? message.content.length > 0
|
|
95
|
-
: message.content.some((part) => part.type === "text")));
|
|
96
|
-
if (lastMessage) lastMessage.providerOptions = {
|
|
97
|
-
...lastMessage.providerOptions,
|
|
98
|
-
openrouter: { ...lastMessage.providerOptions?.openrouter, cacheControl },
|
|
99
|
-
};
|
|
100
|
-
}
|
|
105
|
+
if (cacheControl) markCacheBoundary(boundary, cacheControl);
|
|
101
106
|
return result;
|
|
102
107
|
}
|
|
@@ -261,8 +261,19 @@ function applyRequestCompatibility(payload: unknown, model: Model<"openai-comple
|
|
|
261
261
|
}
|
|
262
262
|
if (cacheControl && body.tools?.length) body.tools[body.tools.length - 1].cache_control = cacheControl;
|
|
263
263
|
if (!body.tools && body.messages.some((message) => message.role === "tool")) body.tools = [];
|
|
264
|
+
const marks = cacheMarksOf(body);
|
|
265
|
+
if (marks > MAX_CACHE_MARKS) throw new Error(`The request carries ${marks} cache marks; Anthropic accepts at most ${MAX_CACHE_MARKS}.`);
|
|
264
266
|
}
|
|
265
267
|
|
|
268
|
+
const MAX_CACHE_MARKS = 4;
|
|
269
|
+
|
|
270
|
+
/** Counts cache_control blocks anywhere in the request body, as the provider counts them. */
|
|
271
|
+
export const cacheMarksOf = (value: unknown): number => {
|
|
272
|
+
if (Array.isArray(value)) return value.reduce((sum: number, item) => sum + cacheMarksOf(item), 0);
|
|
273
|
+
if (!value || typeof value !== "object") return 0;
|
|
274
|
+
return Object.entries(value).reduce((sum, [key, item]) => sum + (key === "cache_control" && item ? 1 : cacheMarksOf(item)), 0);
|
|
275
|
+
};
|
|
276
|
+
|
|
266
277
|
export const streamSimple: StreamFunction<"openai-completions", SimpleStreamOptions> = (model, context, options) => {
|
|
267
278
|
const reasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
|
|
268
279
|
return stream(model, context, {
|
|
@@ -31,7 +31,7 @@ export function transformMessages<TApi extends Api>(
|
|
|
31
31
|
// Build a map of original tool call IDs to normalized IDs
|
|
32
32
|
const toolCallIdMap = new Map<string, string>();
|
|
33
33
|
// Normalize null/undefined content from untyped callers (custom tools, hand-built
|
|
34
|
-
// histories
|
|
34
|
+
// histories) so downstream code can rely on the type contract.
|
|
35
35
|
const normalizedMessages = messages.map((msg) => (msg.content == null ? { ...msg, content: [] } : msg));
|
|
36
36
|
validateInputModalities(normalizedMessages, model);
|
|
37
37
|
|
package/packages/ai/src/types.ts
CHANGED
|
@@ -261,6 +261,8 @@ export interface UserMessage {
|
|
|
261
261
|
role: "user";
|
|
262
262
|
content: string | UserContent[];
|
|
263
263
|
timestamp: number; // Unix timestamp in milliseconds
|
|
264
|
+
/** A note for this one request only; the cache boundary stays in front of it. */
|
|
265
|
+
transient?: true;
|
|
264
266
|
}
|
|
265
267
|
|
|
266
268
|
export interface AssistantMessage {
|
|
@@ -97,5 +97,19 @@ export function isRetryableAssistantError(message: AssistantMessage): boolean {
|
|
|
97
97
|
if (message.stopReason !== "error" || !message.errorMessage) return false;
|
|
98
98
|
const errorMessage = message.errorMessage;
|
|
99
99
|
if (NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN.test(errorMessage)) return false;
|
|
100
|
+
if (isDeterministicClientError(errorMessage)) return false;
|
|
100
101
|
return RETRYABLE_PROVIDER_ERROR_PATTERN.test(errorMessage);
|
|
101
102
|
}
|
|
103
|
+
|
|
104
|
+
const RETRYABLE_CLIENT_STATUSES = new Set([408, 409, 429]);
|
|
105
|
+
|
|
106
|
+
/** The status leads the formatted error ("400: body", "Relay url (400): body"), sits in a JSON body ("code"/"status": 400) or follows "HTTP". */
|
|
107
|
+
const ERROR_STATUS_PATTERN = /^(\d{3}): |\((\d{3})\): |"(?:code|status)"\s*:\s*(\d{3})\b|\bHTTP\s*(\d{3})\b/i;
|
|
108
|
+
|
|
109
|
+
/** A rejected request fails the same way again: 4xx other than timeout, conflict and throttling, or Anthropic's invalid_request_error. */
|
|
110
|
+
function isDeterministicClientError(errorMessage: string): boolean {
|
|
111
|
+
if (/invalid_request_error/.test(errorMessage)) return true;
|
|
112
|
+
const match = ERROR_STATUS_PATTERN.exec(errorMessage);
|
|
113
|
+
const status = Number(match?.slice(1).find((group) => group !== undefined));
|
|
114
|
+
return status >= 400 && status < 500 && !RETRYABLE_CLIENT_STATUSES.has(status);
|
|
115
|
+
}
|
|
@@ -61,6 +61,21 @@ const eventPayloads = {
|
|
|
61
61
|
"turn.interrupted": payloadOf<"turn.interrupted">({
|
|
62
62
|
turnId: Type.String({ description: "ID des unterbrochenen Turns" }),
|
|
63
63
|
}),
|
|
64
|
+
"model.input.presented": payloadOf<"model.input.presented">({
|
|
65
|
+
turnId: Type.String({ description: "Turn in which the model was given the input" }),
|
|
66
|
+
}),
|
|
67
|
+
"model.step.completed": payloadOf<"model.step.completed">({
|
|
68
|
+
turnId: Type.String({ description: "Turn the model step belongs to" }),
|
|
69
|
+
stopReason: Type.String(),
|
|
70
|
+
}),
|
|
71
|
+
"model.tool-result.presented": payloadOf<"model.tool-result.presented">({
|
|
72
|
+
turnId: Type.String(),
|
|
73
|
+
toolCallId: Type.String({ description: "ID of the tool call" }),
|
|
74
|
+
}),
|
|
75
|
+
"context.compacted": payloadOf<"context.compacted">({
|
|
76
|
+
turnId: Type.String({ description: "Turn in which the context was compacted" }),
|
|
77
|
+
tokensBefore: Type.Integer(),
|
|
78
|
+
}),
|
|
64
79
|
"model.output.completed": payloadOf<"model.output.completed">({
|
|
65
80
|
turnId: Type.String({ description: "Turn, zu dem die Ausgabe gehört" }),
|
|
66
81
|
}),
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import {
|
|
2
|
+
contextMessages,
|
|
3
|
+
latestCompaction,
|
|
4
|
+
type AgentMessage,
|
|
5
|
+
type ContextLogEntry,
|
|
6
|
+
} from "@ragents/agent";
|
|
7
|
+
import type { AssistantMessage, ImageContent, TextContent, ToolResultMessage, UserContent, UserMessage } from "@ragents/ai";
|
|
8
|
+
|
|
9
|
+
import type { EventPayloads, JournalEvent, ModelInputContent, ModelToolResultPart } from "../domain/events.ts";
|
|
10
|
+
import type { JsonValue } from "../domain/json.ts";
|
|
11
|
+
import { pluginStateKey } from "../domain/model.ts";
|
|
12
|
+
import { derivedToolResultText } from "../domain/tool-result-text.ts";
|
|
13
|
+
import type { ModelContextRecord, TurnRequest } from "../drivers/types.ts";
|
|
14
|
+
import type { Orchestration } from "../runtime/orchestration.ts";
|
|
15
|
+
import type { ClaimedTurn } from "./turn.ts";
|
|
16
|
+
|
|
17
|
+
/** Reads the bytes behind a stored media hash as Base64. */
|
|
18
|
+
export type MediaSource = (hash: string) => string;
|
|
19
|
+
|
|
20
|
+
/** The model context of one actor, projected from the journal; `key` changes with every compaction. */
|
|
21
|
+
export type ModelContext = {
|
|
22
|
+
readonly entries: readonly ContextLogEntry[];
|
|
23
|
+
readonly messages: readonly AgentMessage[];
|
|
24
|
+
readonly key: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type ToolEvent = Extract<JournalEvent, { type: "tool.call.completed" | "tool.call.failed" }>;
|
|
28
|
+
|
|
29
|
+
const timeOf = (event: JournalEvent) => Date.parse(event.occurredAt);
|
|
30
|
+
|
|
31
|
+
const userContentOf = (content: ModelInputContent, media: MediaSource): string | UserContent[] =>
|
|
32
|
+
typeof content === "string"
|
|
33
|
+
? content
|
|
34
|
+
: content.map((part): UserContent => {
|
|
35
|
+
if (part.type === "text") return part;
|
|
36
|
+
if (part.type === "file") return { type: "file", data: media(part.hash), mimeType: part.mimeType, filename: part.filename };
|
|
37
|
+
return { type: part.type, data: media(part.hash), mimeType: part.mimeType };
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const toolResultContentOf = (content: readonly ModelToolResultPart[], media: MediaSource): (TextContent | ImageContent)[] =>
|
|
41
|
+
content.map((part) => part.type === "text" ? part : { type: "image", data: media(part.hash), mimeType: part.mimeType });
|
|
42
|
+
|
|
43
|
+
const assistantMessageOf = (
|
|
44
|
+
event: Extract<JournalEvent, { type: "model.step.completed" }>,
|
|
45
|
+
observed: readonly JournalEvent[],
|
|
46
|
+
): AssistantMessage => {
|
|
47
|
+
const texts = observed.filter((entry) => entry.type === "model.output.completed").map((entry) => entry.payload.text as string);
|
|
48
|
+
const thoughts = observed.filter((entry) => entry.type === "model.reasoning.completed").map((entry) => entry.payload.text as string);
|
|
49
|
+
const next = (queue: string[], kind: string) => {
|
|
50
|
+
const value = queue.shift();
|
|
51
|
+
|
|
52
|
+
if (value === undefined)
|
|
53
|
+
throw new Error(`Model step ${event.eventId} misses the ${kind} of one of its blocks in its command.`);
|
|
54
|
+
|
|
55
|
+
return value;
|
|
56
|
+
};
|
|
57
|
+
const { turnId: _turnId, content, ...step } = event.payload;
|
|
58
|
+
const message: AssistantMessage = {
|
|
59
|
+
role: "assistant",
|
|
60
|
+
...step,
|
|
61
|
+
content: content.map((block) => {
|
|
62
|
+
if (block.type === "text")
|
|
63
|
+
return block.text === undefined ? { ...block, text: next(texts, "text") } : { ...block, text: block.text };
|
|
64
|
+
|
|
65
|
+
if (block.type === "thinking")
|
|
66
|
+
return block.thinking === undefined ? { ...block, thinking: next(thoughts, "reasoning") } : { ...block, thinking: block.thinking };
|
|
67
|
+
|
|
68
|
+
return block;
|
|
69
|
+
}),
|
|
70
|
+
} as unknown as AssistantMessage;
|
|
71
|
+
|
|
72
|
+
if (texts.length > 0 || thoughts.length > 0)
|
|
73
|
+
throw new Error(`Model step ${event.eventId} has observation events that none of its blocks takes.`);
|
|
74
|
+
|
|
75
|
+
return message;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** The context log of one actor from its own events, built event by event. */
|
|
79
|
+
class OwnContextLog {
|
|
80
|
+
readonly entries: ContextLogEntry[] = [];
|
|
81
|
+
readonly #actorId: string;
|
|
82
|
+
readonly #media: MediaSource;
|
|
83
|
+
readonly #toolResults = new Map<string, ToolEvent>();
|
|
84
|
+
readonly #observedByCommand = new Map<string, JournalEvent[]>();
|
|
85
|
+
|
|
86
|
+
constructor(actorId: string, media: MediaSource) {
|
|
87
|
+
this.#actorId = actorId;
|
|
88
|
+
this.#media = media;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Takes one event of the run; true when the context of the actor changed. */
|
|
92
|
+
apply(event: JournalEvent): boolean {
|
|
93
|
+
if (event.actorId !== this.#actorId)
|
|
94
|
+
return false;
|
|
95
|
+
|
|
96
|
+
switch (event.type) {
|
|
97
|
+
case "tool.call.completed":
|
|
98
|
+
case "tool.call.failed":
|
|
99
|
+
this.#toolResults.set(`${event.payload.turnId}\0${event.payload.toolCallId}`, event);
|
|
100
|
+
return false;
|
|
101
|
+
|
|
102
|
+
case "model.output.completed":
|
|
103
|
+
case "model.reasoning.completed":
|
|
104
|
+
this.#observedByCommand.set(event.commandId, [...(this.#observedByCommand.get(event.commandId) ?? []), event]);
|
|
105
|
+
return false;
|
|
106
|
+
|
|
107
|
+
case "model.input.presented": {
|
|
108
|
+
const message: UserMessage = { role: "user", content: userContentOf(event.payload.content, this.#media), timestamp: timeOf(event) };
|
|
109
|
+
this.entries.push({ kind: "message", id: event.eventId, message });
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
case "model.step.completed":
|
|
114
|
+
this.entries.push({ kind: "message", id: event.eventId, message: assistantMessageOf(event, this.#observedByCommand.get(event.commandId) ?? []) });
|
|
115
|
+
this.#observedByCommand.delete(event.commandId);
|
|
116
|
+
return true;
|
|
117
|
+
|
|
118
|
+
case "model.tool-result.presented": {
|
|
119
|
+
const key = `${event.payload.turnId}\0${event.payload.toolCallId}`;
|
|
120
|
+
this.entries.push({ kind: "message", id: event.eventId, message: toolResultMessageOf(event, this.#toolResults, this.#media) });
|
|
121
|
+
this.#toolResults.delete(key);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
case "context.compacted":
|
|
126
|
+
this.entries.push({
|
|
127
|
+
kind: "compaction",
|
|
128
|
+
id: event.eventId,
|
|
129
|
+
summary: event.payload.summary,
|
|
130
|
+
firstKeptId: event.payload.firstKeptEventId,
|
|
131
|
+
tokensBefore: event.payload.tokensBefore,
|
|
132
|
+
timestamp: timeOf(event),
|
|
133
|
+
readFiles: [...event.payload.readFiles],
|
|
134
|
+
modifiedFiles: [...event.payload.modifiedFiles],
|
|
135
|
+
});
|
|
136
|
+
return true;
|
|
137
|
+
|
|
138
|
+
default:
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const ownEntries = (events: readonly JournalEvent[], actorId: string, media: MediaSource): ContextLogEntry[] => {
|
|
145
|
+
const log = new OwnContextLog(actorId, media);
|
|
146
|
+
|
|
147
|
+
for (const event of events)
|
|
148
|
+
log.apply(event);
|
|
149
|
+
|
|
150
|
+
return log.entries;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const toolResultMessageOf = (
|
|
154
|
+
event: Extract<JournalEvent, { type: "model.tool-result.presented" }>,
|
|
155
|
+
toolResults: ReadonlyMap<string, ToolEvent>,
|
|
156
|
+
media: MediaSource,
|
|
157
|
+
): ToolResultMessage => {
|
|
158
|
+
const payload: EventPayloads["model.tool-result.presented"] = event.payload;
|
|
159
|
+
let content: (TextContent | ImageContent)[];
|
|
160
|
+
|
|
161
|
+
if (payload.content) {
|
|
162
|
+
content = toolResultContentOf(payload.content, media);
|
|
163
|
+
} else {
|
|
164
|
+
const result = toolResults.get(`${payload.turnId}\0${payload.toolCallId}`);
|
|
165
|
+
|
|
166
|
+
if (!result)
|
|
167
|
+
throw new Error(`Tool result ${event.eventId} names call ${payload.toolCallId}, which has no recorded result.`);
|
|
168
|
+
|
|
169
|
+
content = [{ type: "text", text: derivedToolResultText(result) }];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
role: "toolResult",
|
|
174
|
+
toolCallId: payload.toolCallId,
|
|
175
|
+
toolName: payload.toolName,
|
|
176
|
+
content,
|
|
177
|
+
isError: payload.isError,
|
|
178
|
+
timestamp: timeOf(event),
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/** Where a fork cuts its source: before the source's turn that was still running at the spawn, else at the spawn. */
|
|
183
|
+
const forkCut = (events: readonly JournalEvent[], source: string, spawnSequence: number): number => {
|
|
184
|
+
const running = new Map<string, number>();
|
|
185
|
+
|
|
186
|
+
for (const event of events) {
|
|
187
|
+
if (event.sequence >= spawnSequence)
|
|
188
|
+
break;
|
|
189
|
+
|
|
190
|
+
if (event.type === "turn.started" && event.actorId === source)
|
|
191
|
+
running.set(event.payload.turnId, event.sequence);
|
|
192
|
+
else if (event.type === "turn.finished" || event.type === "turn.interrupted")
|
|
193
|
+
running.delete(event.payload.turnId);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return Math.min(spawnSequence, ...running.values());
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/** What a fork took over from its source: an unchanged copy of the context of the source's finished turns. */
|
|
200
|
+
const inheritedEntries = (events: readonly JournalEvent[], actorId: string, media: MediaSource): ContextLogEntry[] => {
|
|
201
|
+
const spawn = events.find((event): event is Extract<JournalEvent, { type: "agent.spawned" }> =>
|
|
202
|
+
event.type === "agent.spawned" && event.payload.agentId === actorId);
|
|
203
|
+
const source = spawn?.payload.forkOf;
|
|
204
|
+
|
|
205
|
+
if (!source)
|
|
206
|
+
return [];
|
|
207
|
+
|
|
208
|
+
const cut = forkCut(events, source, spawn.sequence);
|
|
209
|
+
const inherited = modelContextLog(events.filter((event) => event.sequence < cut), source, media);
|
|
210
|
+
|
|
211
|
+
if (inherited.length === 0)
|
|
212
|
+
throw new Error(`Fork source ${source} had no model context from a finished turn at the spawn.`);
|
|
213
|
+
|
|
214
|
+
return inherited;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/** The context log of an actor, including the part a fork took over from its source at its spawn. */
|
|
218
|
+
export const modelContextLog = (events: readonly JournalEvent[], actorId: string, media: MediaSource): ContextLogEntry[] =>
|
|
219
|
+
[...inheritedEntries(events, actorId, media), ...ownEntries(events, actorId, media)];
|
|
220
|
+
|
|
221
|
+
const contextOf = (entries: ContextLogEntry[], conversation: string): ModelContext => ({
|
|
222
|
+
entries,
|
|
223
|
+
messages: contextMessages(entries),
|
|
224
|
+
key: `${conversation}:${latestCompaction(entries)?.id ?? ""}`,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
/** The model context of an actor as a pure projection of the journal; no rendering, only stored forms. */
|
|
228
|
+
export const modelContextOf = (events: readonly JournalEvent[], actorId: string, media: MediaSource): ModelContext =>
|
|
229
|
+
contextOf(modelContextLog(events, actorId, media), events.find((event) => event.type === "run.created")?.eventId ?? "");
|
|
230
|
+
|
|
231
|
+
const deepFreeze = <T>(value: T): T => {
|
|
232
|
+
if (value === null || typeof value !== "object" || Object.isFrozen(value))
|
|
233
|
+
return value;
|
|
234
|
+
|
|
235
|
+
for (const entry of Object.values(value))
|
|
236
|
+
deepFreeze(entry);
|
|
237
|
+
|
|
238
|
+
return Object.freeze(value);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/** The events of a run as the journal holds them; the sequence counts from 1 without gaps. */
|
|
242
|
+
export type ModelContextSource = {
|
|
243
|
+
eventsSince: (runId: string, sequence: number) => readonly JournalEvent[];
|
|
244
|
+
firstEventId: (runId: string) => string | undefined;
|
|
245
|
+
media: MediaSource;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
type CachedContext = {
|
|
249
|
+
conversation: string;
|
|
250
|
+
sequence: number;
|
|
251
|
+
inherited: ContextLogEntry[];
|
|
252
|
+
own: OwnContextLog;
|
|
253
|
+
context: ModelContext | null;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/** The model contexts of actors in a turn, extended by new events only; always equal to `modelContextOf` over the journal. */
|
|
257
|
+
export class ModelContexts {
|
|
258
|
+
readonly #source: ModelContextSource;
|
|
259
|
+
readonly #runs = new Map<string, Map<string, CachedContext>>();
|
|
260
|
+
|
|
261
|
+
constructor(source: ModelContextSource) {
|
|
262
|
+
this.#source = source;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
contextOf(runId: string, actorId: string): ModelContext {
|
|
266
|
+
const actors = this.#runs.get(runId) ?? new Map<string, CachedContext>();
|
|
267
|
+
const conversation = this.#source.firstEventId(runId) ?? "";
|
|
268
|
+
let cached = actors.get(actorId);
|
|
269
|
+
|
|
270
|
+
if (!cached || cached.conversation !== conversation) {
|
|
271
|
+
const events = this.#source.eventsSince(runId, 0);
|
|
272
|
+
cached = {
|
|
273
|
+
conversation,
|
|
274
|
+
sequence: 0,
|
|
275
|
+
inherited: deepFreeze(inheritedEntries(events, actorId, this.#source.media)),
|
|
276
|
+
own: new OwnContextLog(actorId, this.#source.media),
|
|
277
|
+
context: null,
|
|
278
|
+
};
|
|
279
|
+
this.#extend(cached, events);
|
|
280
|
+
} else {
|
|
281
|
+
this.#extend(cached, this.#source.eventsSince(runId, cached.sequence));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
actors.set(actorId, cached);
|
|
285
|
+
this.#runs.set(runId, actors);
|
|
286
|
+
cached.context ??= deepFreeze(contextOf([...cached.inherited, ...cached.own.entries], conversation));
|
|
287
|
+
|
|
288
|
+
return cached.context;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
forget(runId: string) {
|
|
292
|
+
this.#runs.delete(runId);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Drops what an actor holds once its turn ended; the next turn builds its context once from the journal. */
|
|
296
|
+
forgetActor(runId: string, actorId: string) {
|
|
297
|
+
const actors = this.#runs.get(runId);
|
|
298
|
+
actors?.delete(actorId);
|
|
299
|
+
|
|
300
|
+
if (actors?.size === 0)
|
|
301
|
+
this.#runs.delete(runId);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** How many actor contexts are held; only actors in a running turn keep one. */
|
|
305
|
+
get size() {
|
|
306
|
+
return [...this.#runs.values()].reduce((sum, actors) => sum + actors.size, 0);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
#extend(cached: CachedContext, events: readonly JournalEvent[]) {
|
|
310
|
+
for (const event of events) {
|
|
311
|
+
if (event.sequence !== cached.sequence + 1)
|
|
312
|
+
throw new Error(`The model context expected event ${cached.sequence + 1} and got ${event.sequence}.`);
|
|
313
|
+
|
|
314
|
+
cached.sequence = event.sequence;
|
|
315
|
+
|
|
316
|
+
if (cached.own.apply(event))
|
|
317
|
+
cached.context = null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** How a turn of an LLM actor reads and writes its model context in the journal; `next` numbers the commands of the turn. */
|
|
323
|
+
export const turnModelContext = (
|
|
324
|
+
runtime: Orchestration,
|
|
325
|
+
turn: ClaimedTurn,
|
|
326
|
+
next: () => number,
|
|
327
|
+
): Pick<TurnRequest<"agent">, "modelContext" | "recordContext" | "hookState"> => {
|
|
328
|
+
const { runId, actorId, turnId } = turn;
|
|
329
|
+
const context = (kind: string) => ({ actorId, commandId: `scheduler:${turnId}:context:${kind}:${next()}`, turnId, correlationId: turnId });
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
modelContext: () => runtime.modelContext(runId, actorId),
|
|
333
|
+
recordContext: (entry: ModelContextRecord) => {
|
|
334
|
+
if (entry.kind === "input")
|
|
335
|
+
runtime.presentModelInput(context(entry.kind), runId, actorId, { turnId, inputId: entry.inputId, content: entry.content });
|
|
336
|
+
else if (entry.kind === "step")
|
|
337
|
+
runtime.completeModelStep(context(entry.kind), runId, actorId, { turnId, step: entry.step });
|
|
338
|
+
else if (entry.kind === "tool-result")
|
|
339
|
+
runtime.presentToolResult(context(entry.kind), runId, actorId, {
|
|
340
|
+
turnId,
|
|
341
|
+
toolCallId: entry.toolCallId,
|
|
342
|
+
toolName: entry.toolName,
|
|
343
|
+
isError: entry.isError,
|
|
344
|
+
content: entry.content,
|
|
345
|
+
});
|
|
346
|
+
else
|
|
347
|
+
runtime.compactContext(context(entry.kind), runId, actorId, { turnId, ...entry.compaction });
|
|
348
|
+
},
|
|
349
|
+
hookState: {
|
|
350
|
+
kept: (hookId: string) => runtime.select(runId, (state) => state.pluginStates.get(pluginStateKey(hookId, { kind: "actor", actorId }))?.state),
|
|
351
|
+
keep: (hookId: string, value: JsonValue) => {
|
|
352
|
+
runtime.replacePluginState(
|
|
353
|
+
{ actorId: runtime.select(runId, (state) => state.ownerId), commandId: `scheduler:${turnId}:hook:${hookId}:${next()}`, turnId },
|
|
354
|
+
runId,
|
|
355
|
+
{ pluginId: hookId, scope: { kind: "actor", actorId }, state: value },
|
|
356
|
+
);
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
};
|
|
@@ -19,6 +19,8 @@ export type ToolContributor = {
|
|
|
19
19
|
name: string;
|
|
20
20
|
descriptors: readonly ToolDescriptor[];
|
|
21
21
|
dynamic?: boolean;
|
|
22
|
+
/** Ob die Werkzeuge in einem Run gelten; ohne Angabe in jedem, sonst fehlen sie dort ganz. */
|
|
23
|
+
runCondition?: (runId: string) => boolean;
|
|
22
24
|
tools: (context: PluginContext) => readonly RunFunction[] | Promise<readonly RunFunction[]>;
|
|
23
25
|
};
|
|
24
26
|
|
|
@@ -74,6 +76,9 @@ export class ToolRegistry implements ToolProvider {
|
|
|
74
76
|
const taken = new Set(agentTools.map((entry) => entry.name));
|
|
75
77
|
|
|
76
78
|
for (const plugin of this.#plugins) {
|
|
79
|
+
if (plugin.runCondition && !plugin.runCondition(context.runId))
|
|
80
|
+
continue;
|
|
81
|
+
|
|
77
82
|
const tools = await plugin.tools(context);
|
|
78
83
|
this.#assertDescriptors(plugin, tools);
|
|
79
84
|
|