@springbrand/agent-runtime 0.3.0-alpha.16 → 0.3.0-alpha.18
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/package.json +6 -4
- package/src/adapter/cloudflare/agent.ts +81 -123
- package/src/adapter/cloudflare/subagent/definition.ts +21 -9
- package/src/adapter/cloudflare/subagent/runner.ts +42 -49
- package/src/adapter/cloudflare/subagent/tools.ts +2 -2
- package/src/config/harness-runtime.ts +28 -15
- package/src/config/model-guidance.ts +16 -17
- package/src/config/ports.ts +2 -0
- package/src/conversation/harness-resume.ts +69 -234
- package/src/conversation/harness-stream.ts +85 -70
- package/src/conversation/history.ts +153 -153
- package/src/conversation/message/contract.ts +12 -0
- package/src/conversation/session-entry.ts +29 -0
- package/src/harness/governance.ts +206 -0
- package/src/harness/index.ts +3 -0
- package/src/harness/session-harness.ts +251 -607
- package/src/harness/submission-operation.ts +15 -40
- package/src/models/index.ts +14 -3
- package/src/models/openrouter/protocol-response.ts +36 -112
- package/src/session/cloudflare/migration.ts +382 -924
- package/src/session/cloudflare/storage.ts +13 -536
- package/src/session/index.ts +22 -35
- package/src/submission/finalization.ts +29 -13
- package/src/submission/repository.ts +1 -22
- package/src/submission/steer-delivery.ts +32 -76
- package/src/telemetry/turn-events.ts +34 -24
- package/src/tools/runtime/compiler.ts +32 -50
- package/src/tools/runtime/decision-gate.ts +5 -5
|
@@ -1,49 +1,24 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
1
|
+
import type { Context } from "@earendil-works/chord";
|
|
2
|
+
import type { EntryId } from "@earendil-works/pi-durable";
|
|
3
3
|
import type { PiCanonicalUserInput } from "../conversation/message/contract";
|
|
4
4
|
import type { StoredSubmission } from "../submission/repository";
|
|
5
|
-
import type { SessionHarness } from "./session-harness";
|
|
5
|
+
import type { SessionHarness, SessionRunRequest } from "./session-harness";
|
|
6
6
|
|
|
7
|
-
/**
|
|
7
|
+
/** Reacquire Pi's stable Submission before admitting another model run. */
|
|
8
8
|
export async function prepareSubmissionOperation(
|
|
9
|
-
runtime: SessionHarness
|
|
9
|
+
runtime: SessionHarness,
|
|
10
10
|
submission: Pick<StoredSubmission, "submissionId" | "queuedInputJson" | "regenerateMessageId">,
|
|
11
|
-
regenerationTarget: (messageId: string) =>
|
|
11
|
+
regenerationTarget: (messageId: string) => Promise<EntryId | null>,
|
|
12
12
|
context: Context,
|
|
13
|
-
): Promise<
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
if (
|
|
17
|
-
const navigation = await runtime.run(undefined, context);
|
|
18
|
-
if (navigation.kind !== "settled" || navigation.outcome.status !== "completed") {
|
|
19
|
-
throw new Error("Could not recover the regeneration branch navigation");
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
if (runtime.openOperation) {
|
|
23
|
-
if (runtime.openOperation.operationId !== id) {
|
|
24
|
-
throw new Error(`Submission ${id} does not own the open Harness operation`);
|
|
25
|
-
}
|
|
26
|
-
return { kind: "run" };
|
|
27
|
-
}
|
|
28
|
-
const watch = await runtime.lane.watch(context);
|
|
29
|
-
const previous = watch.snapshot.lastResult;
|
|
30
|
-
watch.unsubscribe();
|
|
31
|
-
if (previous?.operationId === id) return { kind: "settled", outcome: previous };
|
|
32
|
-
if (!submission.queuedInputJson) {
|
|
33
|
-
throw new Error(`Submission ${id} has no open Harness operation or queued input`);
|
|
34
|
-
}
|
|
13
|
+
): Promise<SessionRunRequest> {
|
|
14
|
+
const existing = await runtime.submissionFor(submission.submissionId, context);
|
|
15
|
+
if (existing) return { operationId: submission.submissionId };
|
|
16
|
+
if (!submission.queuedInputJson) throw new Error("The admitted Submission has no canonical input");
|
|
35
17
|
const prompt = JSON.parse(submission.queuedInputJson) as PiCanonicalUserInput;
|
|
36
|
-
if (prompt.role !== "user") throw new Error("
|
|
37
|
-
if (submission.regenerateMessageId
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
operationId: navigationId,
|
|
41
|
-
targetId: regenerationTarget(submission.regenerateMessageId),
|
|
42
|
-
options: { summarize: false },
|
|
43
|
-
}, context);
|
|
44
|
-
if (navigation.kind !== "settled" || navigation.outcome.status !== "completed") {
|
|
45
|
-
throw new Error("Could not navigate to the regeneration branch");
|
|
46
|
-
}
|
|
18
|
+
if (prompt.role !== "user") throw new Error("Invalid canonical user input");
|
|
19
|
+
if (submission.regenerateMessageId) {
|
|
20
|
+
const target = await regenerationTarget(submission.regenerateMessageId);
|
|
21
|
+
await runtime.fork(target, context);
|
|
47
22
|
}
|
|
48
|
-
return {
|
|
23
|
+
return { operationId: submission.submissionId, prompt };
|
|
49
24
|
}
|
package/src/models/index.ts
CHANGED
|
@@ -537,14 +537,15 @@ export function modelRequestUrl(model: Model<Api>): string {
|
|
|
537
537
|
*/
|
|
538
538
|
export function withProviderBoundary(
|
|
539
539
|
streamFn: ProviderBoundStream,
|
|
540
|
-
|
|
540
|
+
/** Host-owned provider Session; it overrides the durable Harness's per-conversation identity. */
|
|
541
|
+
hostSessionId?: string | (() => string),
|
|
541
542
|
onGeneration?: PiGenerationLifecycleObserver,
|
|
542
543
|
): ProviderBoundStream {
|
|
543
544
|
return (model, context, options) =>
|
|
544
545
|
lazyStream(model, async () => {
|
|
545
546
|
const requestId = crypto.randomUUID();
|
|
546
547
|
const startedAt = Date.now();
|
|
547
|
-
const sessionId = options?.sessionId
|
|
548
|
+
const sessionId = (typeof hostSessionId === "function" ? hostSessionId() : hostSessionId) ?? options?.sessionId;
|
|
548
549
|
const url = modelRequestUrl(model);
|
|
549
550
|
const systemPrompt = "systemPrompt" in context && typeof context.systemPrompt === "string"
|
|
550
551
|
? context.systemPrompt
|
|
@@ -910,11 +911,15 @@ function configuredModel(
|
|
|
910
911
|
...(endpoint.protocol === "openrouter-messages"
|
|
911
912
|
? {
|
|
912
913
|
...anthropicMessagesCompat,
|
|
914
|
+
// The direct Anthropic catalog enables positional system and Tool updates, but
|
|
915
|
+
// OpenRouter Messages rejects `tool_addition` definitions. Fold them into the
|
|
916
|
+
// leading system and top-level tools so deferred Tool decoration still applies.
|
|
917
|
+
supportsMidConvoSystemMessages: false,
|
|
918
|
+
supportsMidConvoToolChanges: false,
|
|
913
919
|
supportsMidConvoEffort: false,
|
|
914
920
|
supportsEagerToolInputStreaming: false,
|
|
915
921
|
supportsStrictTools:
|
|
916
922
|
anthropicMessagesCompat?.supportsStrictTools ?? false,
|
|
917
|
-
supportsToolReferences: true,
|
|
918
923
|
...(modelId === "anthropic/claude-sonnet-5.5" ||
|
|
919
924
|
modelId === "anthropic/claude-opus-5.5"
|
|
920
925
|
? { forceAdaptiveThinking: true }
|
|
@@ -923,6 +928,9 @@ function configuredModel(
|
|
|
923
928
|
: endpoint.protocol === "openrouter-chat"
|
|
924
929
|
? {
|
|
925
930
|
...catalogCompat,
|
|
931
|
+
// OpenRouter Chat has no verified positional system or Tool update support.
|
|
932
|
+
supportsMidConvoSystemMessages: false,
|
|
933
|
+
supportsMidConvoToolChanges: false,
|
|
926
934
|
thinkingFormat: "openrouter" as const,
|
|
927
935
|
maxTokensField: "max_tokens" as const,
|
|
928
936
|
supportsStore: false,
|
|
@@ -942,6 +950,9 @@ function configuredModel(
|
|
|
942
950
|
}),
|
|
943
951
|
openRouterRouting: {
|
|
944
952
|
require_parameters: true,
|
|
953
|
+
...(endpoint.openRouterProviderSort
|
|
954
|
+
? { sort: endpoint.openRouterProviderSort }
|
|
955
|
+
: {}),
|
|
945
956
|
...(openRouterProviderPin
|
|
946
957
|
? {
|
|
947
958
|
order: [openRouterProviderPin],
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
fauxAssistantMessage,
|
|
2
|
+
lazyStream,
|
|
4
3
|
type Api,
|
|
5
4
|
type AssistantMessage,
|
|
6
5
|
type AssistantMessageEventStream,
|
|
@@ -13,15 +12,11 @@ import { overlayProviderStreams } from "../provider-streams";
|
|
|
13
12
|
|
|
14
13
|
export type OpenRouterProtocol = Extract<RuntimeModelProtocol, `openrouter-${string}`>;
|
|
15
14
|
|
|
16
|
-
/**
|
|
15
|
+
/** Native output required by OpenRouter's stateless continuation. */
|
|
17
16
|
export interface ModelProtocolResponse {
|
|
18
17
|
readonly output: readonly Readonly<Record<string, unknown>>[];
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
interface ProtocolEvent {
|
|
22
|
-
readonly data: unknown;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
20
|
declare module "@earendil-works/pi-ai" {
|
|
26
21
|
interface AssistantMessage {
|
|
27
22
|
protocolResponse?: ModelProtocolResponse;
|
|
@@ -34,113 +29,46 @@ function record(value: unknown): Record<string, unknown> | undefined {
|
|
|
34
29
|
: undefined;
|
|
35
30
|
}
|
|
36
31
|
|
|
37
|
-
function parseProtocolEvents(body: string): ProtocolEvent[] {
|
|
38
|
-
return body.split(/\r?\n\r?\n/).flatMap((frame) => {
|
|
39
|
-
const lines = frame.split(/\r?\n/);
|
|
40
|
-
const dataLines = lines.filter((line) => line.startsWith("data:"))
|
|
41
|
-
.map((line) => line.slice(5).replace(/^ /, ""));
|
|
42
|
-
if (dataLines.length === 0) return [];
|
|
43
|
-
const dataText = dataLines.join("\n");
|
|
44
|
-
let data: unknown = dataText;
|
|
45
|
-
try {
|
|
46
|
-
data = JSON.parse(dataText);
|
|
47
|
-
} catch {
|
|
48
|
-
data = dataText;
|
|
49
|
-
}
|
|
50
|
-
return [{ data }];
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function compactResponsesBody(body: string): ModelProtocolResponse | undefined {
|
|
55
|
-
const completedItems = new Map<number, Record<string, unknown>>();
|
|
56
|
-
let terminalItems: Record<string, unknown>[] | undefined;
|
|
57
|
-
let terminal = false;
|
|
58
|
-
for (const frame of parseProtocolEvents(body)) {
|
|
59
|
-
const event = record(frame.data);
|
|
60
|
-
if (
|
|
61
|
-
event?.type === "response.output_item.done" &&
|
|
62
|
-
typeof event.output_index === "number"
|
|
63
|
-
) {
|
|
64
|
-
const item = record(event.item);
|
|
65
|
-
if (item) completedItems.set(event.output_index, item);
|
|
66
|
-
}
|
|
67
|
-
if (event?.type === "response.completed" || event?.type === "response.incomplete") {
|
|
68
|
-
terminal = true;
|
|
69
|
-
const response = record(event.response);
|
|
70
|
-
if (Array.isArray(response?.output)) {
|
|
71
|
-
terminalItems = response.output.flatMap((value) => {
|
|
72
|
-
const output = record(value);
|
|
73
|
-
return output ? [output] : [];
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
if (!terminal) return undefined;
|
|
79
|
-
const output = terminalItems?.length
|
|
80
|
-
? terminalItems.map((item, index) => ({ ...completedItems.get(index), ...item }))
|
|
81
|
-
: [...completedItems.entries()]
|
|
82
|
-
.sort(([left], [right]) => left - right)
|
|
83
|
-
.map(([, item]) => item);
|
|
84
|
-
return output.length > 0 ? { output } : undefined;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
32
|
function streamWithProtocolResponse<T extends StreamOptions>(
|
|
88
33
|
model: Model<Api>,
|
|
89
34
|
options: T | undefined,
|
|
90
35
|
start: (options: T) => AssistantMessageEventStream,
|
|
91
36
|
): AssistantMessageEventStream {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
37
|
+
return lazyStream(model, async () => {
|
|
38
|
+
let nativeResponse: ModelProtocolResponse | undefined;
|
|
39
|
+
const source = start({
|
|
40
|
+
...options,
|
|
41
|
+
onProviderStreamEvent: async (data: unknown, activeModel: Model<Api>) => {
|
|
42
|
+
const event = record(data);
|
|
43
|
+
if (event?.type === "response.created") nativeResponse = undefined;
|
|
44
|
+
if (event?.type === "response.completed" || event?.type === "response.incomplete") {
|
|
45
|
+
const output = record(event.response)?.output;
|
|
46
|
+
if (Array.isArray(output)) {
|
|
47
|
+
const items = output.flatMap((value) => {
|
|
48
|
+
const item = record(value);
|
|
49
|
+
return item ? [item] : [];
|
|
50
|
+
});
|
|
51
|
+
nativeResponse = items.length > 0 ? { output: items } : undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
await options?.onProviderStreamEvent?.(data, activeModel);
|
|
55
|
+
},
|
|
56
|
+
} as T);
|
|
57
|
+
return (async function* () {
|
|
58
|
+
let terminal = false;
|
|
59
|
+
for await (const event of source) {
|
|
60
|
+
if (event.type === "done" || event.type === "error") {
|
|
61
|
+
terminal = true;
|
|
62
|
+
if (nativeResponse) {
|
|
63
|
+
const message = event.type === "done" ? event.message : event.error;
|
|
64
|
+
message.protocolResponse = nativeResponse;
|
|
65
|
+
}
|
|
116
66
|
}
|
|
67
|
+
yield event;
|
|
117
68
|
}
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
if (!terminal) throw new Error("Provider stream ended without a terminal event");
|
|
121
|
-
output.end();
|
|
122
|
-
})().catch(async (error: unknown) => {
|
|
123
|
-
if (terminal) {
|
|
124
|
-
output.end();
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
const reason = options?.signal?.aborted ? "aborted" : "error";
|
|
128
|
-
const failure = fauxAssistantMessage("", {
|
|
129
|
-
stopReason: reason,
|
|
130
|
-
errorMessage: error instanceof Error ? error.message : String(error),
|
|
131
|
-
});
|
|
132
|
-
if (responseOutput) {
|
|
133
|
-
const compact = await responseOutput;
|
|
134
|
-
if (compact) failure.protocolResponse = compact;
|
|
135
|
-
}
|
|
136
|
-
output.push({
|
|
137
|
-
type: "error",
|
|
138
|
-
reason,
|
|
139
|
-
error: { ...failure, api: model.api, provider: model.provider, model: model.id },
|
|
140
|
-
});
|
|
141
|
-
output.end();
|
|
69
|
+
if (!terminal) throw new Error("Provider stream ended without a terminal event");
|
|
70
|
+
})();
|
|
142
71
|
});
|
|
143
|
-
return output;
|
|
144
72
|
}
|
|
145
73
|
|
|
146
74
|
export function withModelProtocolResponse(
|
|
@@ -150,12 +78,8 @@ export function withModelProtocolResponse(
|
|
|
150
78
|
if (protocol !== "openrouter-responses") return api;
|
|
151
79
|
return overlayProviderStreams(api, {
|
|
152
80
|
stream: (model, context, options) =>
|
|
153
|
-
streamWithProtocolResponse(model, options, (requestOptions) =>
|
|
154
|
-
api.stream(model, context, requestOptions)
|
|
155
|
-
),
|
|
81
|
+
streamWithProtocolResponse(model, options, (requestOptions) => api.stream(model, context, requestOptions)),
|
|
156
82
|
streamSimple: (model, context, options) =>
|
|
157
|
-
streamWithProtocolResponse(model, options, (requestOptions) =>
|
|
158
|
-
api.streamSimple(model, context, requestOptions)
|
|
159
|
-
),
|
|
83
|
+
streamWithProtocolResponse(model, options, (requestOptions) => api.streamSimple(model, context, requestOptions)),
|
|
160
84
|
});
|
|
161
85
|
}
|