@springbrand/agent-runtime 0.3.0-alpha.15 → 0.3.0-alpha.17

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.
@@ -1,49 +1,24 @@
1
- import type { OperationRequest, OperationResultRecord } from "@earendil-works/pi-agent-core";
2
- import type { Context } from "@earendil-works/pi-agent-core/harness/context";
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
- /** Recover the stable operation before accepting new input or navigating its branch. */
7
+ /** Reacquire Pi's stable Submission before admitting another model run. */
8
8
  export async function prepareSubmissionOperation(
9
- runtime: SessionHarness<object | undefined>,
9
+ runtime: SessionHarness,
10
10
  submission: Pick<StoredSubmission, "submissionId" | "queuedInputJson" | "regenerateMessageId">,
11
- regenerationTarget: (messageId: string) => string | null,
11
+ regenerationTarget: (messageId: string) => Promise<EntryId | null>,
12
12
  context: Context,
13
- ): Promise<{ kind: "run"; request?: OperationRequest } | { kind: "settled"; outcome: OperationResultRecord }> {
14
- const id = submission.submissionId;
15
- const navigationId = `${id}:regenerate`;
16
- if (runtime.openOperation?.operationId === navigationId) {
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("Queued SpringBrand user message is invalid");
37
- if (submission.regenerateMessageId && previous?.operationId !== navigationId) {
38
- const navigation = await runtime.run({
39
- kind: "navigation",
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 { kind: "run", request: { kind: "prompt", operationId: id, prompt } };
23
+ return { operationId: submission.submissionId, prompt };
49
24
  }
@@ -914,7 +914,6 @@ function configuredModel(
914
914
  supportsEagerToolInputStreaming: false,
915
915
  supportsStrictTools:
916
916
  anthropicMessagesCompat?.supportsStrictTools ?? false,
917
- supportsToolReferences: true,
918
917
  ...(modelId === "anthropic/claude-sonnet-5.5" ||
919
918
  modelId === "anthropic/claude-opus-5.5"
920
919
  ? { forceAdaptiveThinking: true }
@@ -942,6 +941,9 @@ function configuredModel(
942
941
  }),
943
942
  openRouterRouting: {
944
943
  require_parameters: true,
944
+ ...(endpoint.openRouterProviderSort
945
+ ? { sort: endpoint.openRouterProviderSort }
946
+ : {}),
945
947
  ...(openRouterProviderPin
946
948
  ? {
947
949
  order: [openRouterProviderPin],
@@ -1,6 +1,5 @@
1
1
  import {
2
- createAssistantMessageEventStream,
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
- /** Minimal native output retained for the next OpenRouter Responses request. */
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
- const output = createAssistantMessageEventStream();
93
- const fetcher = options?.fetch ?? globalThis.fetch;
94
- let responseOutput: Promise<ModelProtocolResponse | undefined> | undefined;
95
- let terminal = false;
96
- const requestOptions = {
97
- ...options,
98
- fetch: (async (...args: Parameters<typeof fetch>) => {
99
- const response = await fetcher(...args);
100
- responseOutput = response.clone().text()
101
- .then((body) => compactResponsesBody(body))
102
- .catch(() => undefined);
103
- return response;
104
- }) satisfies typeof fetch,
105
- } as T;
106
-
107
- void (async () => {
108
- const source = start(requestOptions);
109
- for await (const event of source) {
110
- if (event.type === "done" || event.type === "error") {
111
- terminal = true;
112
- if (responseOutput) {
113
- const message = event.type === "done" ? event.message : event.error;
114
- const compact = await responseOutput;
115
- if (compact) message.protocolResponse = compact;
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
- output.push(event);
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
  }