@schlenkr/ragents 0.1.6 → 0.1.7
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/provider.ts +34 -11
- package/apps/server/src/ragents/actor-chat-history.ts +1 -0
- package/apps/server/src/ragents/engine.ts +5 -13
- 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-CFhTJEmH.js} +1 -1
- package/apps/web/dist/assets/{main-BCjvD2Hh.js → main-DlzW4t4c.js} +1 -1
- package/apps/web/dist/assets/{mermaid-HWGCJPDP-CvYYFK_n.js → mermaid-HWGCJPDP-kxkB991i.js} +69 -69
- package/apps/web/dist/assets/{runPanel-CG7kRMJh.js → runPanel-BJEFQ__w.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 +3 -3
- 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/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/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 +7 -6
- package/packages/ragents/src/plugin-host.ts +3 -4
- 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/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
|
@@ -544,13 +544,13 @@ export const agentTools: RunFunction[] = [
|
|
|
544
544
|
+ "It inherits delegable capabilities, but tools is required and never inherited: select exact names, [] for text-only work, or explicit null for an open, dynamically resolved toolset. "
|
|
545
545
|
+ "An empty tools array creates a plain LLM with no runtime, workspace or host tools; "
|
|
546
546
|
+ "drivers without plain-LLM isolation are rejected. "
|
|
547
|
-
+ "forkOf
|
|
547
|
+
+ "forkOf gives the new agent an unchanged copy of the model context of an existing LLM agent of this run, up to the end of that agent's last finished turn; nothing of its running turn is copied, and a source without a finished turn is rejected. The new agent still gets its own prompt, tools and model, and its first input follows the copy.",
|
|
548
548
|
schema: Type.Object({
|
|
549
549
|
handle: Type.String({ minLength: 1 }),
|
|
550
550
|
displayName: Type.Optional(Type.String({ minLength: 1, description: "Anzeigename; ohne Angabe der Handle" })),
|
|
551
551
|
description: Type.Optional(Type.String({ minLength: 1, maxLength: actorDescriptionMaxLength, description: "Sehr kurze Beschreibung der Aufgabe für die Übersicht der Beteiligten, wenige Wörter wie \"prüft die Regel zu Kommentaren\"" })),
|
|
552
552
|
prompt: Type.String(),
|
|
553
|
-
forkOf: Type.Optional(Type.String({ minLength: 1, description: "Handle
|
|
553
|
+
forkOf: Type.Optional(Type.String({ minLength: 1, description: "Handle or ID of an LLM agent of this run whose model context up to the end of its last finished turn is copied into the new agent" })),
|
|
554
554
|
tools: Type.Union([Type.Array(Type.String({ minLength: 1 }), { uniqueItems: true }), Type.Null()], { description: "Required explicit selection: [] for plain text-only work including app-mediated conversations; an array for exact existing tool names; null only when the task needs an open, dynamically resolved toolset. Never inherits the caller's tools. Names of future, not yet activated actor functions are invalid; choose null when those must become available later." }),
|
|
555
555
|
withoutCapabilities: Type.Optional(Type.Array(capabilitySchema, { uniqueItems: true })),
|
|
556
556
|
...executionSchema,
|
|
@@ -164,18 +164,46 @@ export class TurnToolset {
|
|
|
164
164
|
this.tools.splice(0, this.tools.length, ...this.functions.filter((tool) => tool.nativeTool === true));
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/** A model's tool call always lands in the journal: a failure before the start is recorded as start and failure. */
|
|
167
168
|
async invoke(toolCallId: string, name: string, input: JsonValue, modelContext?: string): Promise<ToolInvocation> {
|
|
168
|
-
await this
|
|
169
|
-
|
|
169
|
+
const prepared = await this.#beforeStart(toolCallId, name, input, async () => {
|
|
170
|
+
await this.refresh();
|
|
171
|
+
const tool = this.tools.find((entry) => entry.name === name);
|
|
170
172
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
+
if (!tool)
|
|
174
|
+
throw new Error(`Tool ${name} is not available to actor ${this.#turn.actorId}.`);
|
|
173
175
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
176
|
+
assertJsonValue(input, `Tool ${name} input`);
|
|
177
|
+
const tolerated = withoutUnknownFields(tool.schema, input);
|
|
178
|
+
|
|
179
|
+
if (!this.#replayOf(toolCallId) && !Value.Check(tool.schema, tolerated.input))
|
|
180
|
+
throw new Error(inputComplaint(tool, tolerated.input, tolerated.ignoredFields));
|
|
181
|
+
|
|
182
|
+
return { tool, tolerated };
|
|
183
|
+
});
|
|
184
|
+
const output = await this.#execute(toolCallId, prepared.tool, prepared.tolerated.input, prepared.tolerated.ignoredFields, modelContext);
|
|
185
|
+
|
|
186
|
+
return { output, ignoredFields: prepared.tolerated.ignoredFields };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async #beforeStart<T>(toolCallId: string, name: string, input: JsonValue, prepare: () => Promise<T>): Promise<T> {
|
|
190
|
+
try {
|
|
191
|
+
return await prepare();
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (this.#replayOf(toolCallId))
|
|
194
|
+
throw error;
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
this.#assertActive();
|
|
198
|
+
assertJsonValue(input, `Tool ${name} input`);
|
|
199
|
+
} catch {
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
177
202
|
|
|
178
|
-
|
|
203
|
+
this.#runtime.startToolCall(this.#context(toolCallId, "started"), this.#turn.runId, this.#turn.actorId, { turnId: this.#turn.turnId, toolCallId, name, input });
|
|
204
|
+
this.#runtime.failToolCall(this.#context(toolCallId, "failed"), this.#turn.runId, this.#turn.actorId, { turnId: this.#turn.turnId, toolCallId, name, error: errorText(error) });
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
179
207
|
}
|
|
180
208
|
|
|
181
209
|
async invokeFunction(toolCallId: string, name: string, input: JsonValue): Promise<JsonValue> {
|
|
@@ -282,18 +310,17 @@ export class TurnToolset {
|
|
|
282
310
|
}
|
|
283
311
|
|
|
284
312
|
#replayOf(toolCallId: string) {
|
|
285
|
-
const events
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
});
|
|
313
|
+
const events: ToolCallEvent[] = [];
|
|
314
|
+
|
|
315
|
+
for (const event of this.#runtime.recentEvents(this.#turn.runId)) {
|
|
316
|
+
if (event.type === "turn.started" && event.payload.turnId === this.#turn.turnId)
|
|
317
|
+
break;
|
|
318
|
+
|
|
319
|
+
if ((event.type === "tool.call.started" || event.type === "tool.call.completed" || event.type === "tool.call.failed")
|
|
320
|
+
&& event.actorId === this.#turn.actorId && event.payload.turnId === this.#turn.turnId && event.payload.toolCallId === toolCallId)
|
|
321
|
+
events.unshift(event);
|
|
322
|
+
}
|
|
323
|
+
|
|
297
324
|
const started = events.find((event) => event.type === "tool.call.started");
|
|
298
325
|
const terminal = events.findLast(
|
|
299
326
|
(event) => event.type === "tool.call.completed" || event.type === "tool.call.failed",
|
|
@@ -2,8 +2,6 @@ export interface Workspaces {
|
|
|
2
2
|
ensure(runId: string, workspacePath: string | null): string;
|
|
3
3
|
/** How the run's workspace is described to a model; undefined when nothing describes it. */
|
|
4
4
|
description(runId: string): string | undefined;
|
|
5
|
-
/** The folder on this machine where a driver keeps its own work; the workspace itself may lie on another machine. */
|
|
6
|
-
runtimeDirectory(runId: string): Promise<string>;
|
|
7
5
|
/** Stores an attached file under attachments/ of the workspace, on the machine where the workspace lies; returns its name there. */
|
|
8
6
|
storeAttachment(runId: string, name: string, content: Uint8Array): Promise<string>;
|
|
9
7
|
}
|
|
@@ -23,10 +21,6 @@ export class FixedWorkspaces implements Workspaces {
|
|
|
23
21
|
return undefined;
|
|
24
22
|
}
|
|
25
23
|
|
|
26
|
-
runtimeDirectory() {
|
|
27
|
-
return Promise.resolve(this.#path);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
24
|
storeAttachment(): Promise<string> {
|
|
31
25
|
return Promise.reject(new Error("Ein fester Arbeitsbereich ohne Executor nimmt keine Dateianhänge an."));
|
|
32
26
|
}
|
|
@@ -20,13 +20,18 @@ export type EventSemanticContext = {
|
|
|
20
20
|
events: Map<string, JournalEvent>;
|
|
21
21
|
/** Tool calls with a recorded source, by turn and call; their state stands in the turn. */
|
|
22
22
|
sourcedToolCalls: Set<string>;
|
|
23
|
+
/** Inputs and tool results already presented to the model, by turn and input or call. */
|
|
24
|
+
presented: Set<string>;
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
export const eventSemanticContext = (): EventSemanticContext => ({
|
|
26
28
|
events: new Map(),
|
|
27
29
|
sourcedToolCalls: new Set(),
|
|
30
|
+
presented: new Set(),
|
|
28
31
|
});
|
|
29
32
|
|
|
33
|
+
const modelContextEventTypes: ReadonlySet<string> = new Set(["model.input.presented", "model.step.completed", "model.tool-result.presented"]);
|
|
34
|
+
|
|
30
35
|
const existingActorOf = (state: RunState, actorId: string): Actor => {
|
|
31
36
|
const actor = state.actors.get(actorId);
|
|
32
37
|
|
|
@@ -438,6 +443,46 @@ export const assertEventSemantics = (
|
|
|
438
443
|
break;
|
|
439
444
|
}
|
|
440
445
|
|
|
446
|
+
case "model.input.presented": {
|
|
447
|
+
const turn = runningTurnOf(state, event.payload.turnId);
|
|
448
|
+
assertTurnAuthor(turn, event);
|
|
449
|
+
|
|
450
|
+
if (event.payload.inputId !== null) {
|
|
451
|
+
const input = state.inputs.get(event.payload.inputId);
|
|
452
|
+
|
|
453
|
+
if (!input || input.lifecycle.kind !== "claimed" || input.lifecycle.turnId !== turn.id)
|
|
454
|
+
throw new Error(`Input ${event.payload.inputId} does not belong to turn ${turn.id}.`);
|
|
455
|
+
|
|
456
|
+
if (context.presented.has(`${turn.id}\0input\0${input.id}`))
|
|
457
|
+
throw new Error(`Input ${input.id} was already presented in turn ${turn.id}.`);
|
|
458
|
+
}
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
case "model.tool-result.presented": {
|
|
463
|
+
const turn = runningTurnOf(state, event.payload.turnId);
|
|
464
|
+
assertTurnAuthor(turn, event);
|
|
465
|
+
const call = toolCallOf(turn, event.payload.toolCallId);
|
|
466
|
+
|
|
467
|
+
if (call.status === "running")
|
|
468
|
+
throw new Error(`Tool call ${call.id} in turn ${turn.id} has no result yet.`);
|
|
469
|
+
|
|
470
|
+
if (context.presented.has(`${turn.id}\0call\0${call.id}`))
|
|
471
|
+
throw new Error(`The result of tool call ${call.id} in turn ${turn.id} was already presented.`);
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
case "context.compacted": {
|
|
476
|
+
const turn = runningTurnOf(state, event.payload.turnId);
|
|
477
|
+
assertTurnAuthor(turn, event);
|
|
478
|
+
const firstKept = context.events.get(event.payload.firstKeptEventId);
|
|
479
|
+
|
|
480
|
+
if (!firstKept || !modelContextEventTypes.has(firstKept.type))
|
|
481
|
+
throw new Error(`Compaction keeps from ${event.payload.firstKeptEventId}, which is no model context event.`);
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
case "model.step.completed":
|
|
441
486
|
case "model.output.completed":
|
|
442
487
|
case "model.output.interrupted":
|
|
443
488
|
case "model.reasoning.completed":
|
|
@@ -653,4 +698,10 @@ export const recordEventSemantics = (event: JournalEvent, context: EventSemantic
|
|
|
653
698
|
|
|
654
699
|
if (event.type === "tool.call.source")
|
|
655
700
|
context.sourcedToolCalls.add(toolCallKey(event.payload.turnId, event.payload.toolCallId));
|
|
701
|
+
|
|
702
|
+
if (event.type === "model.input.presented" && event.payload.inputId !== null)
|
|
703
|
+
context.presented.add(`${event.payload.turnId}\0input\0${event.payload.inputId}`);
|
|
704
|
+
|
|
705
|
+
if (event.type === "model.tool-result.presented")
|
|
706
|
+
context.presented.add(`${event.payload.turnId}\0call\0${event.payload.toolCallId}`);
|
|
656
707
|
};
|
|
@@ -200,6 +200,98 @@ const usageOf = (value: unknown, path: string) => {
|
|
|
200
200
|
fail(`${path}.costUsd`, "must not be negative");
|
|
201
201
|
};
|
|
202
202
|
|
|
203
|
+
const sha256Of = (value: unknown, path: string) => {
|
|
204
|
+
if (typeof value !== "string" || !/^[0-9a-f]{64}$/.test(value))
|
|
205
|
+
fail(path, "must be a lowercase SHA-256");
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const modelTextPartOf = (value: unknown, path: string) => {
|
|
209
|
+
const part = exactObject(value, path, ["type", "text"]);
|
|
210
|
+
stringOf(part.text, `${path}.text`);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const modelInputPartOf = (value: unknown, path: string) => {
|
|
214
|
+
const candidate = objectOf(value, path);
|
|
215
|
+
|
|
216
|
+
if (candidate.type === "text")
|
|
217
|
+
return modelTextPartOf(value, path);
|
|
218
|
+
|
|
219
|
+
if (candidate.type !== "image" && candidate.type !== "video" && candidate.type !== "file")
|
|
220
|
+
fail(`${path}.type`, "must be text, image, video or file");
|
|
221
|
+
|
|
222
|
+
const part = exactObject(value, path, candidate.type === "file" ? ["type", "mimeType", "filename", "hash"] : ["type", "mimeType", "hash"]);
|
|
223
|
+
nonEmptyStringOf(part.mimeType, `${path}.mimeType`);
|
|
224
|
+
sha256Of(part.hash, `${path}.hash`);
|
|
225
|
+
|
|
226
|
+
if (candidate.type === "file")
|
|
227
|
+
stringOf(part.filename, `${path}.filename`);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const modelToolResultPartOf = (value: unknown, path: string) => {
|
|
231
|
+
const candidate = objectOf(value, path);
|
|
232
|
+
|
|
233
|
+
if (candidate.type === "text")
|
|
234
|
+
return modelTextPartOf(value, path);
|
|
235
|
+
|
|
236
|
+
if (candidate.type !== "image")
|
|
237
|
+
fail(`${path}.type`, "must be text or image");
|
|
238
|
+
|
|
239
|
+
const part = exactObject(value, path, ["type", "mimeType", "hash"]);
|
|
240
|
+
nonEmptyStringOf(part.mimeType, `${path}.mimeType`);
|
|
241
|
+
sha256Of(part.hash, `${path}.hash`);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const optionalStringOf = (payload: ObjectValue, key: string, path: string) => {
|
|
245
|
+
if (Object.hasOwn(payload, key))
|
|
246
|
+
stringOf(payload[key], `${path}.${key}`);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const modelStepBlockOf = (value: unknown, path: string) => {
|
|
250
|
+
const candidate = objectOf(value, path);
|
|
251
|
+
|
|
252
|
+
if (candidate.type === "text") {
|
|
253
|
+
const block = exactObject(value, path, ["type"], ["text", "textSignature"]);
|
|
254
|
+
optionalStringOf(block, "text", path);
|
|
255
|
+
optionalStringOf(block, "textSignature", path);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (candidate.type === "thinking") {
|
|
260
|
+
const block = exactObject(value, path, ["type"], ["thinking", "thinkingSignature", "redacted"]);
|
|
261
|
+
optionalStringOf(block, "thinking", path);
|
|
262
|
+
optionalStringOf(block, "thinkingSignature", path);
|
|
263
|
+
|
|
264
|
+
if (Object.hasOwn(block, "redacted"))
|
|
265
|
+
booleanOf(block.redacted, `${path}.redacted`);
|
|
266
|
+
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (candidate.type !== "toolCall")
|
|
271
|
+
fail(`${path}.type`, "must be text, thinking or toolCall");
|
|
272
|
+
|
|
273
|
+
const block = exactObject(value, path, ["type", "id", "name", "arguments"], ["thoughtSignature"]);
|
|
274
|
+
nonEmptyStringOf(block.id, `${path}.id`);
|
|
275
|
+
stringOf(block.name, `${path}.name`);
|
|
276
|
+
objectOf(block.arguments, `${path}.arguments`);
|
|
277
|
+
assertJsonValue(block.arguments, `${path}.arguments`);
|
|
278
|
+
optionalStringOf(block, "thoughtSignature", path);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const modelStepUsageOf = (value: unknown, path: string) => {
|
|
282
|
+
const usage = exactObject(value, path, ["input", "output", "cacheRead", "cacheWrite", "totalTokens", "cost"], ["cacheWrite1h", "reasoning"]);
|
|
283
|
+
|
|
284
|
+
for (const key of ["input", "output", "cacheRead", "cacheWrite", "totalTokens", "cacheWrite1h", "reasoning"]) {
|
|
285
|
+
if (Object.hasOwn(usage, key))
|
|
286
|
+
finiteNumberOf(usage[key], `${path}.${key}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const cost = exactObject(usage.cost, `${path}.cost`, ["input", "output", "cacheRead", "cacheWrite", "total"]);
|
|
290
|
+
|
|
291
|
+
for (const key of Object.keys(cost))
|
|
292
|
+
finiteNumberOf(cost[key], `${path}.cost.${key}`);
|
|
293
|
+
};
|
|
294
|
+
|
|
203
295
|
const pluginScopeOf = (value: unknown, path: string) => {
|
|
204
296
|
const candidate = objectOf(value, path);
|
|
205
297
|
|
|
@@ -391,6 +483,80 @@ const payloadOf = (type: EventType, value: unknown, path: string) => {
|
|
|
391
483
|
return;
|
|
392
484
|
}
|
|
393
485
|
|
|
486
|
+
case "model.input.presented": {
|
|
487
|
+
const payload = exactObject(value, path, ["turnId", "inputId", "content"]);
|
|
488
|
+
stringOf(payload.turnId, `${path}.turnId`);
|
|
489
|
+
nullableStringOf(payload.inputId, `${path}.inputId`);
|
|
490
|
+
|
|
491
|
+
if (typeof payload.content !== "string")
|
|
492
|
+
arrayOf(payload.content, `${path}.content`, modelInputPartOf);
|
|
493
|
+
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
case "model.step.completed": {
|
|
498
|
+
const payload = exactObject(
|
|
499
|
+
value,
|
|
500
|
+
path,
|
|
501
|
+
["turnId", "api", "provider", "model", "usage", "stopReason", "timestamp", "content"],
|
|
502
|
+
["responseModel", "responseId", "errorMessage", "diagnostics"],
|
|
503
|
+
);
|
|
504
|
+
stringOf(payload.turnId, `${path}.turnId`);
|
|
505
|
+
nonEmptyStringOf(payload.api, `${path}.api`);
|
|
506
|
+
nonEmptyStringOf(payload.provider, `${path}.provider`);
|
|
507
|
+
nonEmptyStringOf(payload.model, `${path}.model`);
|
|
508
|
+
optionalStringOf(payload, "responseModel", path);
|
|
509
|
+
optionalStringOf(payload, "responseId", path);
|
|
510
|
+
optionalStringOf(payload, "errorMessage", path);
|
|
511
|
+
modelStepUsageOf(payload.usage, `${path}.usage`);
|
|
512
|
+
|
|
513
|
+
if (!["stop", "length", "toolUse", "error"].includes(payload.stopReason as string))
|
|
514
|
+
fail(`${path}.stopReason`, "must be stop, length, toolUse or error");
|
|
515
|
+
|
|
516
|
+
finiteNumberOf(payload.timestamp, `${path}.timestamp`);
|
|
517
|
+
arrayOf(payload.content, `${path}.content`, modelStepBlockOf);
|
|
518
|
+
|
|
519
|
+
if (Object.hasOwn(payload, "diagnostics"))
|
|
520
|
+
arrayOf(payload.diagnostics, `${path}.diagnostics`, assertJsonValue);
|
|
521
|
+
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
case "model.tool-result.presented": {
|
|
526
|
+
const payload = exactObject(value, path, ["turnId", "toolCallId", "toolName", "isError"], ["content"]);
|
|
527
|
+
stringOf(payload.turnId, `${path}.turnId`);
|
|
528
|
+
nonEmptyStringOf(payload.toolCallId, `${path}.toolCallId`);
|
|
529
|
+
stringOf(payload.toolName, `${path}.toolName`);
|
|
530
|
+
booleanOf(payload.isError, `${path}.isError`);
|
|
531
|
+
|
|
532
|
+
if (Object.hasOwn(payload, "content"))
|
|
533
|
+
arrayOf(payload.content, `${path}.content`, modelToolResultPartOf);
|
|
534
|
+
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
case "context.compacted": {
|
|
539
|
+
const payload = exactObject(value, path, [
|
|
540
|
+
"turnId",
|
|
541
|
+
"summary",
|
|
542
|
+
"firstKeptEventId",
|
|
543
|
+
"tokensBefore",
|
|
544
|
+
"provider",
|
|
545
|
+
"model",
|
|
546
|
+
"readFiles",
|
|
547
|
+
"modifiedFiles",
|
|
548
|
+
]);
|
|
549
|
+
stringOf(payload.turnId, `${path}.turnId`);
|
|
550
|
+
nonEmptyStringOf(payload.summary, `${path}.summary`);
|
|
551
|
+
nonEmptyStringOf(payload.firstKeptEventId, `${path}.firstKeptEventId`);
|
|
552
|
+
nonNegativeIntegerOf(payload.tokensBefore, `${path}.tokensBefore`);
|
|
553
|
+
nonEmptyStringOf(payload.provider, `${path}.provider`);
|
|
554
|
+
nonEmptyStringOf(payload.model, `${path}.model`);
|
|
555
|
+
stringArrayOf(payload.readFiles, `${path}.readFiles`);
|
|
556
|
+
stringArrayOf(payload.modifiedFiles, `${path}.modifiedFiles`);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
394
560
|
case "model.output.completed":
|
|
395
561
|
case "model.output.interrupted":
|
|
396
562
|
case "model.reasoning.completed":
|
|
@@ -17,6 +17,35 @@ import type {
|
|
|
17
17
|
} from "./model.ts";
|
|
18
18
|
import type { JsonChange, JsonObject, JsonValue } from "./json.ts";
|
|
19
19
|
|
|
20
|
+
/** A media part the model saw, stored by the SHA-256 of its bytes in the artifact contents instead of Base64. */
|
|
21
|
+
export type ModelMediaPart =
|
|
22
|
+
| { type: "image" | "video"; mimeType: string; hash: string }
|
|
23
|
+
| { type: "file"; mimeType: string; filename: string; hash: string };
|
|
24
|
+
|
|
25
|
+
export type ModelTextPart = { type: "text"; text: string };
|
|
26
|
+
|
|
27
|
+
/** The content of a user message exactly as the model received it. */
|
|
28
|
+
export type ModelInputContent = string | (ModelTextPart | ModelMediaPart)[];
|
|
29
|
+
|
|
30
|
+
export type ModelToolResultPart = ModelTextPart | { type: "image"; mimeType: string; hash: string };
|
|
31
|
+
|
|
32
|
+
/** A block of a model step; text and thinking without their own field stand in the observation events of the same command, in order. */
|
|
33
|
+
export type ModelStepBlock =
|
|
34
|
+
| { type: "text"; text?: string; textSignature?: string }
|
|
35
|
+
| { type: "thinking"; thinking?: string; thinkingSignature?: string; redacted?: boolean }
|
|
36
|
+
| { type: "toolCall"; id: string; name: string; arguments: JsonObject; thoughtSignature?: string };
|
|
37
|
+
|
|
38
|
+
export type ModelStepUsage = {
|
|
39
|
+
input: number;
|
|
40
|
+
output: number;
|
|
41
|
+
cacheRead: number;
|
|
42
|
+
cacheWrite: number;
|
|
43
|
+
cacheWrite1h?: number;
|
|
44
|
+
reasoning?: number;
|
|
45
|
+
totalTokens: number;
|
|
46
|
+
cost: { input: number; output: number; cacheRead: number; cacheWrite: number; total: number };
|
|
47
|
+
};
|
|
48
|
+
|
|
20
49
|
export type EventPayloads = {
|
|
21
50
|
"run.created": {
|
|
22
51
|
title: string;
|
|
@@ -84,6 +113,45 @@ export type EventPayloads = {
|
|
|
84
113
|
turnId: TurnId;
|
|
85
114
|
reason: string;
|
|
86
115
|
};
|
|
116
|
+
"model.input.presented": {
|
|
117
|
+
turnId: TurnId;
|
|
118
|
+
/** The claimed input, or null for a message of the agent loop itself. */
|
|
119
|
+
inputId: InputId | null;
|
|
120
|
+
content: ModelInputContent;
|
|
121
|
+
};
|
|
122
|
+
"model.step.completed": {
|
|
123
|
+
turnId: TurnId;
|
|
124
|
+
api: string;
|
|
125
|
+
provider: string;
|
|
126
|
+
model: string;
|
|
127
|
+
responseModel?: string;
|
|
128
|
+
responseId?: string;
|
|
129
|
+
usage: ModelStepUsage;
|
|
130
|
+
stopReason: "stop" | "length" | "toolUse" | "error";
|
|
131
|
+
errorMessage?: string;
|
|
132
|
+
diagnostics?: JsonValue[];
|
|
133
|
+
timestamp: number;
|
|
134
|
+
content: ModelStepBlock[];
|
|
135
|
+
};
|
|
136
|
+
"model.tool-result.presented": {
|
|
137
|
+
turnId: TurnId;
|
|
138
|
+
toolCallId: string;
|
|
139
|
+
toolName: string;
|
|
140
|
+
isError: boolean;
|
|
141
|
+
/** Absent when the model saw exactly the text of the call's tool.call.completed or tool.call.failed. */
|
|
142
|
+
content?: ModelToolResultPart[];
|
|
143
|
+
};
|
|
144
|
+
"context.compacted": {
|
|
145
|
+
turnId: TurnId;
|
|
146
|
+
summary: string;
|
|
147
|
+
/** The first context event after the cut; everything before it is replaced by the summary. */
|
|
148
|
+
firstKeptEventId: string;
|
|
149
|
+
tokensBefore: number;
|
|
150
|
+
provider: string;
|
|
151
|
+
model: string;
|
|
152
|
+
readFiles: string[];
|
|
153
|
+
modifiedFiles: string[];
|
|
154
|
+
};
|
|
87
155
|
"model.output.completed": {
|
|
88
156
|
turnId: TurnId;
|
|
89
157
|
text: string;
|
|
@@ -198,6 +266,10 @@ export const eventTypeMap: Record<EventType, true> = {
|
|
|
198
266
|
"turn.input-steered": true,
|
|
199
267
|
"turn.finished": true,
|
|
200
268
|
"turn.interrupted": true,
|
|
269
|
+
"model.input.presented": true,
|
|
270
|
+
"model.step.completed": true,
|
|
271
|
+
"model.tool-result.presented": true,
|
|
272
|
+
"context.compacted": true,
|
|
201
273
|
"model.output.completed": true,
|
|
202
274
|
"model.output.interrupted": true,
|
|
203
275
|
"model.reasoning.completed": true,
|
|
@@ -215,6 +215,8 @@ export type RunState = {
|
|
|
215
215
|
pluginStates: ReadonlyMap<string, PluginState>;
|
|
216
216
|
actions: ReadonlyMap<ActionId, Action>;
|
|
217
217
|
artifacts: ReadonlyMap<ArtifactId, Artifact>;
|
|
218
|
+
/** Turns that presented input to the model, with their actor; only they leave model context behind. */
|
|
219
|
+
contextTurns: ReadonlyMap<TurnId, ActorId>;
|
|
218
220
|
};
|
|
219
221
|
|
|
220
222
|
/** Der Actor, den ein Event betrifft: bei Turn-Enden der Besitzer des Turns, bei Stopp und Neustart der Ziel-Actor, sonst der Schreiber. */
|
|
@@ -241,8 +243,8 @@ export const eventSubjectOf = (state: RunState, event: JournalEvent): ActorId =>
|
|
|
241
243
|
|
|
242
244
|
type Collections = "actors" | "inputs" | "turns" | "subscriptions" | "pluginStates" | "actions" | "artifacts";
|
|
243
245
|
|
|
244
|
-
/** Die Run-Ansicht für Clients;
|
|
245
|
-
export type RunView = Omit<RunState, Collections | "ownerUserId"> & {
|
|
246
|
+
/** Die Run-Ansicht für Clients; Eigentümer und Kontext-Turns bleiben serverseitig und stehen nicht in ihr. */
|
|
247
|
+
export type RunView = Omit<RunState, Collections | "ownerUserId" | "contextTurns"> & {
|
|
246
248
|
actors: Actor[];
|
|
247
249
|
inputs: ActorInput[];
|
|
248
250
|
turns: Turn[];
|
|
@@ -50,6 +50,7 @@ export const forkProjection = (state: RunDraft): RunDraft => ({
|
|
|
50
50
|
subscriptions: new Map(state.subscriptions),
|
|
51
51
|
pluginStates: new Map(state.pluginStates),
|
|
52
52
|
artifacts: new Map(state.artifacts),
|
|
53
|
+
contextTurns: new Map(state.contextTurns),
|
|
53
54
|
});
|
|
54
55
|
|
|
55
56
|
const actor = (state: RunDraft, id: string) => {
|
|
@@ -129,6 +130,7 @@ const created = (event: Extract<JournalEvent, { type: "run.created" }>): RunDraf
|
|
|
129
130
|
pluginStates: new Map(),
|
|
130
131
|
actions: new Map(),
|
|
131
132
|
artifacts: new Map(),
|
|
133
|
+
contextTurns: new Map(),
|
|
132
134
|
});
|
|
133
135
|
|
|
134
136
|
export function applyEvent(
|
|
@@ -349,6 +351,13 @@ export function applyEvent(
|
|
|
349
351
|
case "model.output.interrupted":
|
|
350
352
|
case "runtime.output.recorded":
|
|
351
353
|
case "tool.call.source":
|
|
354
|
+
case "model.step.completed":
|
|
355
|
+
case "model.tool-result.presented":
|
|
356
|
+
case "context.compacted":
|
|
357
|
+
break;
|
|
358
|
+
|
|
359
|
+
case "model.input.presented":
|
|
360
|
+
state.contextTurns.set(event.payload.turnId, event.actorId);
|
|
352
361
|
break;
|
|
353
362
|
|
|
354
363
|
case "actor.tools.opened": {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { JournalEvent } from "./events.ts";
|
|
2
|
+
|
|
3
|
+
type ToolResultEvent = Extract<JournalEvent, { type: "tool.call.completed" | "tool.call.failed" }>;
|
|
4
|
+
|
|
5
|
+
/** The text the model sees of a tool call when no hook replaced it and the tool recorded its full output. */
|
|
6
|
+
export const derivedToolResultText = (event: ToolResultEvent): string =>
|
|
7
|
+
event.type === "tool.call.failed"
|
|
8
|
+
? event.payload.error
|
|
9
|
+
: typeof event.payload.output === "string" ? event.payload.output : JSON.stringify(event.payload.output ?? {});
|
|
@@ -1,40 +1,27 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type {
|
|
2
|
+
AgentContribution,
|
|
3
|
+
AgentContributionContext,
|
|
4
|
+
AgentHookContext,
|
|
5
|
+
ModelCallContext,
|
|
6
|
+
ToolCallOutcome,
|
|
7
|
+
ToolResultReplacement,
|
|
8
|
+
} from "../plugin-types.ts";
|
|
4
9
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
/** A hook of a plugin, bound to one agent; the agent driver calls it directly. */
|
|
11
|
+
export interface AgentHook {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly beforeModelCall?: (call: ModelCallContext) => string | undefined | Promise<string | undefined>;
|
|
14
|
+
readonly afterToolCall?: (outcome: ToolCallOutcome, call: AgentHookContext) =>
|
|
15
|
+
ToolResultReplacement | undefined | Promise<ToolResultReplacement | undefined>;
|
|
16
|
+
}
|
|
9
17
|
|
|
10
|
-
/** The one place that
|
|
11
|
-
export const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const note = await beforeModelCall(agent, {
|
|
21
|
-
...hookContextOf(context),
|
|
22
|
-
kept: stored?.type === "custom" ? stored.data as JsonValue : undefined,
|
|
23
|
-
keep: (value) => api.appendEntry(contribution.id, value),
|
|
24
|
-
});
|
|
25
|
-
if (note === undefined) return undefined;
|
|
26
|
-
return { messages: [...event.messages, { role: "custom" as const, customType: contribution.id, content: note, display: false, timestamp: Date.now() }] };
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
if (afterToolCall) {
|
|
30
|
-
api.on("tool_result", async (event, context) => {
|
|
31
|
-
const replacement = await afterToolCall(agent, { toolName: event.toolName, isError: event.isError }, hookContextOf(context));
|
|
32
|
-
if (replacement === undefined) return undefined;
|
|
33
|
-
return {
|
|
34
|
-
content: replacement.content.map((part) => ({ ...part })),
|
|
35
|
-
...(replacement.isError === undefined ? {} : { isError: replacement.isError }),
|
|
36
|
-
};
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
});
|
|
18
|
+
/** The one place that binds the hooks of a plugin to an agent; plugins never see the agent runtime. */
|
|
19
|
+
export const agentHookOf = (contribution: AgentContribution, agent: AgentContributionContext): AgentHook => {
|
|
20
|
+
const { id, beforeModelCall, afterToolCall } = contribution;
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
id,
|
|
24
|
+
...(beforeModelCall ? { beforeModelCall: (call: ModelCallContext) => beforeModelCall(agent, call) } : {}),
|
|
25
|
+
...(afterToolCall ? { afterToolCall: (outcome: ToolCallOutcome, call: AgentHookContext) => afterToolCall(agent, outcome, call) } : {}),
|
|
26
|
+
};
|
|
27
|
+
};
|