@springbrand/agent-runtime 0.3.0-alpha.18 → 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
CHANGED
|
@@ -1072,7 +1072,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
1072
1072
|
/** Start the native resume handshake from Pi's current lane snapshot. */
|
|
1073
1073
|
async onConnect(connection: Connection): Promise<void> {
|
|
1074
1074
|
this.conversation.disconnect(connection);
|
|
1075
|
-
await this.conversation.
|
|
1075
|
+
await this.conversation.connect(connection);
|
|
1076
1076
|
}
|
|
1077
1077
|
|
|
1078
1078
|
async onClose(connection: Connection, code: number, reason: string, wasClean: boolean): Promise<void> {
|
|
@@ -54,21 +54,28 @@ export class HarnessConversationBridge {
|
|
|
54
54
|
await this.send(session.connection, { type: MessageType.CF_AGENT_USE_CHAT_RESPONSE, id: session.source.requestId, body: JSON.stringify(chunk), done: false, ...(replay ? { replay: true, continuation: false } : {}) });
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
-
|
|
57
|
+
/**
|
|
58
|
+
* A socket just opened: offer the live stream if one exists, otherwise only
|
|
59
|
+
* sync messages. Like Cloudflare's AIChatAgent, an idle connect sends no
|
|
60
|
+
* STREAM_RESUME_NONE. The client answers its own resume probe from the
|
|
61
|
+
* reply that carries its probeId; an unsolicited, probe-less "none" arriving
|
|
62
|
+
* first consumes that pending probe without clearing the client's
|
|
63
|
+
* server-streaming state, which leaves a reconnected Chat stuck streaming
|
|
64
|
+
* after a Turn that ended while it was offline.
|
|
65
|
+
*/
|
|
66
|
+
connect(connection: Connection): Promise<void> {
|
|
67
|
+
return this.resume(connection, undefined, false);
|
|
68
|
+
}
|
|
69
|
+
/** Answer a client's stream-resume request; `announceIdle` is false only for {@link connect}. */
|
|
70
|
+
async resume(connection: Connection, probeId?: string, announceIdle = true): Promise<void> {
|
|
58
71
|
this.disconnect(connection);
|
|
59
72
|
const source = this.active;
|
|
60
|
-
if (!source)
|
|
61
|
-
await this.send(connection, { type: MessageType.CF_AGENT_CHAT_MESSAGES, messages: await this.host.history.browserMessages() });
|
|
62
|
-
await this.send(connection, { type: MessageType.CF_AGENT_STREAM_RESUME_NONE, reason: STREAM_RESUME_NONE_REASONS.IDLE, ...(probeId ? { probeId } : {}) });
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
73
|
+
if (!source) return this.idle(connection, probeId, announceIdle);
|
|
65
74
|
const watch = await source.runtime.watch(BACKGROUND_CONTEXT);
|
|
66
|
-
if (this.active !== source) { await watch.stop(); return this.resume(connection, probeId); }
|
|
75
|
+
if (this.active !== source) { await watch.stop(); return this.resume(connection, probeId, announceIdle); }
|
|
67
76
|
if (!watch.snapshot.run) {
|
|
68
77
|
await watch.stop();
|
|
69
|
-
|
|
70
|
-
await this.send(connection, { type: MessageType.CF_AGENT_STREAM_RESUME_NONE, reason: STREAM_RESUME_NONE_REASONS.IDLE, ...(probeId ? { probeId } : {}) });
|
|
71
|
-
return;
|
|
78
|
+
return this.idle(connection, probeId, announceIdle);
|
|
72
79
|
}
|
|
73
80
|
const messages = await this.host.history.browserSnapshot(watch.snapshot, source.submissionId);
|
|
74
81
|
const replay = this.replayMessage(messages, source.submissionId);
|
|
@@ -77,6 +84,15 @@ export class HarnessConversationBridge {
|
|
|
77
84
|
await this.send(connection, { type: MessageType.CF_AGENT_CHAT_MESSAGES, messages: replay ? messages.filter((message) => message.id !== replay.id) : messages });
|
|
78
85
|
await this.send(connection, { type: MessageType.CF_AGENT_STREAM_RESUMING, id: source.requestId, ...(probeId ? { probeId } : {}) });
|
|
79
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* No Turn is running. "None" goes first: while the client still observes the
|
|
89
|
+
* stream it lost, it keeps its own partial answer over any snapshot that has
|
|
90
|
+
* no more parts, so the settled answer must arrive after it stops observing.
|
|
91
|
+
*/
|
|
92
|
+
private async idle(connection: Connection, probeId: string | undefined, announceIdle: boolean): Promise<void> {
|
|
93
|
+
if (announceIdle) await this.send(connection, { type: MessageType.CF_AGENT_STREAM_RESUME_NONE, reason: STREAM_RESUME_NONE_REASONS.IDLE, ...(probeId ? { probeId } : {}) });
|
|
94
|
+
await this.send(connection, { type: MessageType.CF_AGENT_CHAT_MESSAGES, messages: await this.host.history.browserMessages() });
|
|
95
|
+
}
|
|
80
96
|
async acknowledge(connection: Connection, requestId: string): Promise<void> {
|
|
81
97
|
const session = this.connections.get(connection.id);
|
|
82
98
|
if (!session || session.source.requestId !== requestId || session.started) return;
|
|
@@ -595,6 +595,12 @@ export class PiChunkEncoder {
|
|
|
595
595
|
turnDurationMs: Math.max(0, message.timestamp - this.startedAt),
|
|
596
596
|
turnStatus: terminal.turnStatus,
|
|
597
597
|
...(this.turnId ? { turnId: this.turnId } : {}),
|
|
598
|
+
// The browser keeps this stream copy over a history snapshot that
|
|
599
|
+
// arrives while it still observes the stream, so it must carry the
|
|
600
|
+
// user-stop marker the snapshot projects from `turn-aborted`.
|
|
601
|
+
...(terminal.outcome === "aborted" && message.errorMessage === USER_STOP_REASON
|
|
602
|
+
? { interruptedByUser: true }
|
|
603
|
+
: {}),
|
|
598
604
|
...((publicError ?? terminal.message) &&
|
|
599
605
|
(terminal.outcome !== "aborted" ||
|
|
600
606
|
message.errorMessage !== USER_STOP_REASON)
|
|
@@ -13,7 +13,7 @@ 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; `finish()` awaits
|
|
16
|
+
/** User-stop markers appended after their terminal commit; every `finish()` of that Turn awaits it. */
|
|
17
17
|
private readonly stopMarkers = new Map<string, Promise<unknown>>();
|
|
18
18
|
constructor(private readonly options: {
|
|
19
19
|
submissions: SubmissionRepository;
|
|
@@ -36,7 +36,12 @@ export class SubmissionFinalizer {
|
|
|
36
36
|
message?: string,
|
|
37
37
|
): Promise<StoredSubmission> {
|
|
38
38
|
const initial = this.read(submissionId);
|
|
39
|
-
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
|
+
}
|
|
40
45
|
const aborted = Boolean(initial.abortReason) || outcome === "aborted";
|
|
41
46
|
if (aborted) await this.options.releaseAbortedHarness();
|
|
42
47
|
await finishPendingSteers({
|
|
@@ -58,8 +63,8 @@ export class SubmissionFinalizer {
|
|
|
58
63
|
const completed = this.commit(submissionId, outcome, message, output?.role === "assistant" ? output : undefined);
|
|
59
64
|
const marker = this.stopMarkers.get(submissionId);
|
|
60
65
|
if (marker) {
|
|
61
|
-
this.stopMarkers.delete(submissionId);
|
|
62
66
|
await marker;
|
|
67
|
+
this.stopMarkers.delete(submissionId);
|
|
63
68
|
}
|
|
64
69
|
return completed;
|
|
65
70
|
}
|