@springbrand/agent-runtime 0.3.0-alpha.17 → 0.3.0-alpha.19
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 +1 -1
- package/src/adapter/cloudflare/agent.ts +16 -2
- package/src/adapter/cloudflare/subagent/definition.ts +10 -0
- package/src/adapter/cloudflare/subagent/runner.ts +31 -11
- package/src/config/harness-runtime.ts +1 -1
- package/src/conversation/harness-resume.ts +26 -10
- package/src/conversation/harness-stream.ts +22 -6
- package/src/conversation/history.ts +40 -5
- package/src/conversation/message/projection.ts +6 -0
- package/src/harness/governance.ts +206 -0
- package/src/harness/index.ts +1 -0
- package/src/harness/session-harness.ts +97 -14
- package/src/models/index.ts +11 -2
- package/src/session/cloudflare/migration.ts +225 -49
- package/src/submission/finalization.ts +36 -4
- package/src/tools/runtime/compiler.ts +8 -1
|
@@ -13,12 +13,14 @@ import type { SteerRepository } from "./steer-repository";
|
|
|
13
13
|
|
|
14
14
|
/** Commits the Submission outcome and human/Host facts after accepted steering is preserved. */
|
|
15
15
|
export class SubmissionFinalizer {
|
|
16
|
+
/** User-stop markers appended after their terminal commit; every `finish()` of that Turn awaits it. */
|
|
17
|
+
private readonly stopMarkers = new Map<string, Promise<unknown>>();
|
|
16
18
|
constructor(private readonly options: {
|
|
17
19
|
submissions: SubmissionRepository;
|
|
18
20
|
steers: SteerRepository;
|
|
19
21
|
messageUi: MessageUiRepository;
|
|
20
22
|
transaction<T>(run: () => T): T;
|
|
21
|
-
conversation: Pick<ConversationHistory, "append" | "storedMessages">;
|
|
23
|
+
conversation: Pick<ConversationHistory, "append" | "appendTurnAbortedMarker" | "storedMessages">;
|
|
22
24
|
decisions: Pick<HarnessHumanDecisions, "cancelForSubmission">;
|
|
23
25
|
events: Pick<RuntimeTurnEvents, "submissionTerminal">;
|
|
24
26
|
telemetry: Pick<RuntimeTelemetryCoordinator, "capture">;
|
|
@@ -34,7 +36,12 @@ export class SubmissionFinalizer {
|
|
|
34
36
|
message?: string,
|
|
35
37
|
): Promise<StoredSubmission> {
|
|
36
38
|
const initial = this.read(submissionId);
|
|
37
|
-
if (isTerminalSubmissionStatus(initial.status))
|
|
39
|
+
if (isTerminalSubmissionStatus(initial.status)) {
|
|
40
|
+
// A stop RPC and the ending harness finish the same Turn; whichever loses
|
|
41
|
+
// still waits for the stop marker, so its history broadcast carries it.
|
|
42
|
+
await this.stopMarkers.get(submissionId);
|
|
43
|
+
return initial;
|
|
44
|
+
}
|
|
38
45
|
const aborted = Boolean(initial.abortReason) || outcome === "aborted";
|
|
39
46
|
if (aborted) await this.options.releaseAbortedHarness();
|
|
40
47
|
await finishPendingSteers({
|
|
@@ -53,7 +60,13 @@ export class SubmissionFinalizer {
|
|
|
53
60
|
const output = (await this.options.conversation.storedMessages())
|
|
54
61
|
.filter((entry) => entry.submissionId === submissionId && entry.message.role === "assistant")
|
|
55
62
|
.at(-1)?.message;
|
|
56
|
-
|
|
63
|
+
const completed = this.commit(submissionId, outcome, message, output?.role === "assistant" ? output : undefined);
|
|
64
|
+
const marker = this.stopMarkers.get(submissionId);
|
|
65
|
+
if (marker) {
|
|
66
|
+
await marker;
|
|
67
|
+
this.stopMarkers.delete(submissionId);
|
|
68
|
+
}
|
|
69
|
+
return completed;
|
|
57
70
|
}
|
|
58
71
|
|
|
59
72
|
/** Shares atomic terminal facts with an admitted handoff cancelled before execution. */
|
|
@@ -63,7 +76,8 @@ export class SubmissionFinalizer {
|
|
|
63
76
|
message?: string,
|
|
64
77
|
output?: AssistantMessage,
|
|
65
78
|
): StoredSubmission {
|
|
66
|
-
|
|
79
|
+
let stoppedByUser = false;
|
|
80
|
+
const completed = this.options.transaction(() => {
|
|
67
81
|
// Host admission and history reads can await while an explicit stop wins.
|
|
68
82
|
const latest = this.read(submissionId);
|
|
69
83
|
if (isTerminalSubmissionStatus(latest.status)) return latest;
|
|
@@ -79,6 +93,10 @@ export class SubmissionFinalizer {
|
|
|
79
93
|
const status = effectiveOutcome === "succeeded" ? "completed" : effectiveOutcome === "aborted" ? "aborted" : "error";
|
|
80
94
|
this.options.submissions.updateTerminal(submissionId, status, reason, Date.now());
|
|
81
95
|
const completed = this.read(submissionId);
|
|
96
|
+
stoppedByUser = effectiveOutcome === "aborted" && reason === USER_STOP_REASON;
|
|
97
|
+
if (stoppedByUser && completed.completedAt === null) {
|
|
98
|
+
throw new Error(`Aborted SpringBrand submission has no completion time: ${submissionId}`);
|
|
99
|
+
}
|
|
82
100
|
this.options.events.submissionTerminal(completed);
|
|
83
101
|
this.options.telemetry.capture("turnFinished", {
|
|
84
102
|
submissionId,
|
|
@@ -90,6 +108,20 @@ export class SubmissionFinalizer {
|
|
|
90
108
|
});
|
|
91
109
|
return completed;
|
|
92
110
|
});
|
|
111
|
+
if (stoppedByUser) this.appendStopMarker(submissionId, completed.completedAt!);
|
|
112
|
+
return completed;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The browser projects `interruptedByUser` from this marker and the next model request sees it as user context.
|
|
117
|
+
* Pi history writes are async, so the marker follows the synchronous terminal commit.
|
|
118
|
+
*/
|
|
119
|
+
private appendStopMarker(submissionId: string, completedAt: number): void {
|
|
120
|
+
const marker = this.options.conversation.appendTurnAbortedMarker(submissionId, completedAt).catch((error: unknown) => {
|
|
121
|
+
console.warn("[runtime-stop-marker:failed]", submissionId, error instanceof Error ? error.message : String(error));
|
|
122
|
+
});
|
|
123
|
+
this.stopMarkers.set(submissionId, marker);
|
|
124
|
+
this.options.waitUntil(marker);
|
|
93
125
|
}
|
|
94
126
|
|
|
95
127
|
private read(submissionId: string): StoredSubmission {
|
|
@@ -168,8 +168,15 @@ export function compileHarnessTools(
|
|
|
168
168
|
...(candidate.deferLoading ? { deferLoading: true as const } : {}),
|
|
169
169
|
async execute(args, api, context) {
|
|
170
170
|
const toolCallId = api.callId;
|
|
171
|
+
// AgentTool progress is a replacing snapshot (web_search sends its accumulated text), but Pi output only
|
|
172
|
+
// appends and has no replace operation: append a grown snapshot's suffix, and start a new line otherwise.
|
|
173
|
+
let shown = "";
|
|
171
174
|
const onUpdate = (update: AgentToolResult<unknown>) => {
|
|
172
|
-
|
|
175
|
+
const text = update.content.flatMap((part) => part.type === "text" ? [part.text] : []).join("");
|
|
176
|
+
if (text && text !== shown) {
|
|
177
|
+
api.output(text.startsWith(shown) ? text.slice(shown.length) : shown ? `\n${text}` : text);
|
|
178
|
+
shown = text;
|
|
179
|
+
}
|
|
173
180
|
if (update.details !== undefined) void api.details(update.details, context).catch(() => {});
|
|
174
181
|
};
|
|
175
182
|
const signal = context.abortSignal ?? new AbortController().signal;
|