@yefengr/remote-pi 0.7.12 → 0.7.14
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/dist/index.js +67 -36
- package/dist/index.js.map +1 -1
- package/dist/pairing/owner_storage.d.ts +20 -1
- package/dist/pairing/owner_storage.js +57 -1
- package/dist/pairing/owner_storage.js.map +1 -1
- package/dist/pairing/qr.d.ts +19 -0
- package/dist/pairing/qr.js +57 -1
- package/dist/pairing/qr.js.map +1 -1
- package/dist/pairing/storage.d.ts +2 -2
- package/dist/pairing/storage.js +1 -1
- package/dist/pairing/storage.js.map +1 -1
- package/dist/runtime/pairing_coordinator.d.ts +42 -0
- package/dist/runtime/pairing_coordinator.js +136 -0
- package/dist/runtime/pairing_coordinator.js.map +1 -0
- package/dist/timeline/runtime.d.ts +15 -3
- package/dist/timeline/runtime.js +159 -43
- package/dist/timeline/runtime.js.map +1 -1
- package/dist/timeline/tool_lifecycle.d.ts +36 -0
- package/dist/timeline/tool_lifecycle.js +136 -0
- package/dist/timeline/tool_lifecycle.js.map +1 -0
- package/dist/transport/peer_channel.d.ts +1 -1
- package/dist/transport/peer_channel.js +7 -1
- package/dist/transport/peer_channel.js.map +1 -1
- package/dist/transport/relay_client.d.ts +1 -1
- package/dist/transport/relay_client.js +9 -2
- package/dist/transport/relay_client.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { PeerRecord, PeerWriteReceipt } from "../pairing/storage.js";
|
|
2
|
+
import type { QRSession } from "../pairing/qr.js";
|
|
3
|
+
import type { ClientFrame, ServerFrame } from "../protocol/v2/index.js";
|
|
4
|
+
import type { RelayClient } from "../transport/relay_client.js";
|
|
5
|
+
import { V2PeerChannel, type HostRouteIdentity } from "../transport/peer_channel.js";
|
|
6
|
+
type PairRequest = Extract<ClientFrame, {
|
|
7
|
+
type: "pair_request";
|
|
8
|
+
}>;
|
|
9
|
+
type PairOk = Extract<ServerFrame, {
|
|
10
|
+
type: "pair_ok";
|
|
11
|
+
}>;
|
|
12
|
+
export interface PairingBinding {
|
|
13
|
+
readonly channel: V2PeerChannel;
|
|
14
|
+
}
|
|
15
|
+
export interface PairingCoordinatorDependencies {
|
|
16
|
+
readonly qrSession: QRSession;
|
|
17
|
+
readonly routeIdentity: () => HostRouteIdentity;
|
|
18
|
+
readonly isRelayCurrent: (relay: RelayClient) => boolean;
|
|
19
|
+
readonly attachOwner: (relay: RelayClient, ownerId: string) => PairingBinding | null;
|
|
20
|
+
readonly activeBinding: (ownerId: string) => PairingBinding | undefined;
|
|
21
|
+
readonly addPeer: (record: PeerRecord) => Promise<PeerWriteReceipt>;
|
|
22
|
+
readonly rollbackPeer: (receipt: PeerWriteReceipt) => Promise<unknown>;
|
|
23
|
+
readonly updateEndpoint: (relay: RelayClient) => Promise<boolean>;
|
|
24
|
+
readonly refreshCurrentEndpoint: () => Promise<boolean>;
|
|
25
|
+
readonly buildPairOk: (frame: PairRequest) => PairOk;
|
|
26
|
+
}
|
|
27
|
+
/** Coordinates one-process pairing retries across storage and Relay lifecycles. */
|
|
28
|
+
export declare class PairingCoordinator {
|
|
29
|
+
private readonly deps;
|
|
30
|
+
private readonly attempts;
|
|
31
|
+
constructor(deps: PairingCoordinatorDependencies);
|
|
32
|
+
handle(relay: RelayClient, ownerId: string, frame: PairRequest): Promise<void>;
|
|
33
|
+
abandonInactive(): void;
|
|
34
|
+
private run;
|
|
35
|
+
private rollback;
|
|
36
|
+
private lifecycleIsCurrent;
|
|
37
|
+
private canCommit;
|
|
38
|
+
private reject;
|
|
39
|
+
private sendError;
|
|
40
|
+
private attemptKey;
|
|
41
|
+
}
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { V2PeerChannel } from "../transport/peer_channel.js";
|
|
2
|
+
/** Coordinates one-process pairing retries across storage and Relay lifecycles. */
|
|
3
|
+
export class PairingCoordinator {
|
|
4
|
+
deps;
|
|
5
|
+
attempts = new Map();
|
|
6
|
+
constructor(deps) {
|
|
7
|
+
this.deps = deps;
|
|
8
|
+
}
|
|
9
|
+
async handle(relay, ownerId, frame) {
|
|
10
|
+
if (!this.deps.isRelayCurrent(relay))
|
|
11
|
+
return;
|
|
12
|
+
const key = this.attemptKey(ownerId, frame.id);
|
|
13
|
+
const pending = this.attempts.get(key);
|
|
14
|
+
if (pending) {
|
|
15
|
+
await pending.completion;
|
|
16
|
+
return this.handle(relay, ownerId, frame);
|
|
17
|
+
}
|
|
18
|
+
const reservation = this.deps.qrSession.reserveToken(frame.token, ownerId, frame.id);
|
|
19
|
+
if (reservation.status === "expired" || reservation.status === "consumed" || reservation.status === "unknown") {
|
|
20
|
+
const code = reservation.status === "expired" ? "token_expired" : reservation.status === "consumed" ? "token_consumed" : "token_unknown";
|
|
21
|
+
this.reject(relay, ownerId, frame, code, reservation.status === "consumed" ? "Pairing token is already in use" : "Pairing token is invalid or expired");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (reservation.status === "committed") {
|
|
25
|
+
const activeBinding = this.deps.activeBinding(ownerId);
|
|
26
|
+
if (activeBinding) {
|
|
27
|
+
activeBinding.channel.sendV2(reservation.completion);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const replayBinding = this.deps.attachOwner(relay, ownerId);
|
|
31
|
+
replayBinding?.channel.sendV2(reservation.completion);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (reservation.status !== "reserved")
|
|
35
|
+
return;
|
|
36
|
+
const binding = this.deps.attachOwner(relay, ownerId);
|
|
37
|
+
if (!binding) {
|
|
38
|
+
this.deps.qrSession.releaseToken(reservation.reservation);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
let resolveCompletion;
|
|
42
|
+
const completion = new Promise((resolve) => { resolveCompletion = resolve; });
|
|
43
|
+
const attempt = {
|
|
44
|
+
key,
|
|
45
|
+
ownerId,
|
|
46
|
+
relay,
|
|
47
|
+
binding,
|
|
48
|
+
reservation: reservation.reservation,
|
|
49
|
+
completion,
|
|
50
|
+
resolveCompletion,
|
|
51
|
+
};
|
|
52
|
+
this.attempts.set(key, attempt);
|
|
53
|
+
void this.run(attempt, frame);
|
|
54
|
+
}
|
|
55
|
+
abandonInactive() {
|
|
56
|
+
for (const attempt of [...this.attempts.values()]) {
|
|
57
|
+
if (this.lifecycleIsCurrent(attempt))
|
|
58
|
+
continue;
|
|
59
|
+
this.attempts.delete(attempt.key);
|
|
60
|
+
this.deps.qrSession.releaseToken(attempt.reservation);
|
|
61
|
+
attempt.resolveCompletion(null);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async run(attempt, frame) {
|
|
65
|
+
let completion = null;
|
|
66
|
+
let receipt;
|
|
67
|
+
try {
|
|
68
|
+
receipt = await this.deps.addPeer({ name: frame.device_name, remote_epk: attempt.ownerId, paired_at: new Date().toISOString() });
|
|
69
|
+
if (!this.canCommit(attempt)) {
|
|
70
|
+
await this.rollback(receipt);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// peers.json is durable authority, but do not confirm pairing while the
|
|
74
|
+
// current Relay has explicitly rejected the ACL frame. A later retry of
|
|
75
|
+
// the same request can safely repeat the idempotent write.
|
|
76
|
+
const aclSent = await this.deps.updateEndpoint(attempt.relay);
|
|
77
|
+
if (!aclSent || !this.canCommit(attempt)) {
|
|
78
|
+
await this.rollback(receipt);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const pairOk = this.deps.buildPairOk(frame);
|
|
82
|
+
if (!this.deps.qrSession.commitToken(attempt.reservation, pairOk)) {
|
|
83
|
+
await this.rollback(receipt);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
completion = pairOk;
|
|
87
|
+
attempt.binding.channel.sendV2(pairOk);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
if (receipt)
|
|
91
|
+
await this.rollback(receipt);
|
|
92
|
+
if (this.lifecycleIsCurrent(attempt)) {
|
|
93
|
+
this.sendError(attempt.binding.channel, frame, "internal_error", "Failed to persist pairing");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
if (!completion)
|
|
98
|
+
this.deps.qrSession.releaseToken(attempt.reservation);
|
|
99
|
+
if (this.attempts.get(attempt.key) === attempt)
|
|
100
|
+
this.attempts.delete(attempt.key);
|
|
101
|
+
attempt.resolveCompletion(completion);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async rollback(receipt) {
|
|
105
|
+
try {
|
|
106
|
+
await this.deps.rollbackPeer(receipt);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// A failed compensation leaves peers.json authoritative.
|
|
110
|
+
}
|
|
111
|
+
// A new Relay may have announced the transient write while this old
|
|
112
|
+
// attempt was pending. Always refresh whichever Relay is current now.
|
|
113
|
+
try {
|
|
114
|
+
await this.deps.refreshCurrentEndpoint();
|
|
115
|
+
}
|
|
116
|
+
catch { /* reconnect owns recovery */ }
|
|
117
|
+
}
|
|
118
|
+
lifecycleIsCurrent(attempt) {
|
|
119
|
+
return this.deps.isRelayCurrent(attempt.relay) && this.deps.activeBinding(attempt.ownerId) === attempt.binding;
|
|
120
|
+
}
|
|
121
|
+
canCommit(attempt) {
|
|
122
|
+
return this.lifecycleIsCurrent(attempt) && this.deps.qrSession.isReservationCurrent(attempt.reservation);
|
|
123
|
+
}
|
|
124
|
+
reject(relay, ownerId, frame, code, message) {
|
|
125
|
+
const channel = new V2PeerChannel(relay, ownerId, this.deps.routeIdentity(), () => undefined);
|
|
126
|
+
this.sendError(channel, frame, code, message);
|
|
127
|
+
channel.detach();
|
|
128
|
+
}
|
|
129
|
+
sendError(channel, frame, code, message) {
|
|
130
|
+
channel.sendV2({ protocol_version: 2, type: "pair_error", in_reply_to: frame.id, code, message });
|
|
131
|
+
}
|
|
132
|
+
attemptKey(ownerId, requestId) {
|
|
133
|
+
return `${ownerId}\u0000${requestId}`;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=pairing_coordinator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pairing_coordinator.js","sourceRoot":"","sources":["../../src/runtime/pairing_coordinator.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { type JsonValue, type TimelineEvent } from "../protocol/v2/index.js";
|
|
1
|
+
import type { MessageUpdateEvent, SessionManager, ToolExecutionEndEvent, ToolExecutionStartEvent, ToolExecutionUpdateEvent } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type JsonValue, type TimelineEvent, type TimelinePartial } from "../protocol/v2/index.js";
|
|
3
3
|
export declare const TIMELINE_MARKER: "remote-pi:timeline-v2";
|
|
4
4
|
type MessageRole = "user" | "assistant" | "toolResult";
|
|
5
5
|
export type Correlation = {
|
|
@@ -22,6 +22,7 @@ export type TimelineRuntimeOptions = {
|
|
|
22
22
|
getHistoryGeneration?: () => string;
|
|
23
23
|
onStarted?: (started: TimelineStarted) => void;
|
|
24
24
|
onPublished?: (event: TimelineEvent, correlation: Correlation) => void;
|
|
25
|
+
onPartial?: (partial: TimelinePartial, correlation: Correlation) => void;
|
|
25
26
|
};
|
|
26
27
|
export declare class TimelineRuntime {
|
|
27
28
|
private readonly correlations;
|
|
@@ -29,13 +30,16 @@ export declare class TimelineRuntime {
|
|
|
29
30
|
private readonly pending;
|
|
30
31
|
private readonly pendingByRole;
|
|
31
32
|
private readonly published;
|
|
33
|
+
private readonly toolAssociations;
|
|
32
34
|
private sessionManager;
|
|
33
35
|
private epoch;
|
|
36
|
+
private stateRevision;
|
|
34
37
|
private activeGroupId;
|
|
35
38
|
private active;
|
|
36
39
|
private readonly getHistoryGenerationValue?;
|
|
37
40
|
private readonly onStarted?;
|
|
38
41
|
private readonly onPublished?;
|
|
42
|
+
private readonly onPartial?;
|
|
39
43
|
constructor(options?: TimelineRuntimeOptions);
|
|
40
44
|
attach(sessionManager: SessionManager): void;
|
|
41
45
|
resetSession(sessionManager: SessionManager): void;
|
|
@@ -52,9 +56,18 @@ export declare class TimelineRuntime {
|
|
|
52
56
|
onAgentStart(): void;
|
|
53
57
|
onAgentEnd(): void;
|
|
54
58
|
onMessageStart(message: unknown, sessionManager: SessionManager): TimelineStarted | null;
|
|
59
|
+
onMessageUpdate(event: MessageUpdateEvent, sessionManager: SessionManager): void;
|
|
55
60
|
onMessageEnd(message: unknown, sessionManager: SessionManager): void;
|
|
61
|
+
onToolExecutionStart(event: ToolExecutionStartEvent, sessionManager: SessionManager): void;
|
|
62
|
+
onToolExecutionUpdate(event: ToolExecutionUpdateEvent, sessionManager: SessionManager): void;
|
|
63
|
+
onToolExecutionEnd(event: ToolExecutionEndEvent, sessionManager: SessionManager): void;
|
|
64
|
+
private publishToolPartial;
|
|
56
65
|
recover(sessionManager: SessionManager): TimelineEvent[];
|
|
66
|
+
private findPending;
|
|
67
|
+
private messageIdentity;
|
|
57
68
|
private publish;
|
|
69
|
+
private publishPartial;
|
|
70
|
+
private partialDeltaChunks;
|
|
58
71
|
private correlationFor;
|
|
59
72
|
private buildMarker;
|
|
60
73
|
private parseMarker;
|
|
@@ -71,6 +84,5 @@ export declare class TimelineRuntime {
|
|
|
71
84
|
private textFromContent;
|
|
72
85
|
private nonEmpty;
|
|
73
86
|
private base64Length;
|
|
74
|
-
private jsonValue;
|
|
75
87
|
}
|
|
76
88
|
export {};
|
package/dist/timeline/runtime.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { MarkerSchemaV2, TimelineEventSchema
|
|
3
|
+
import { MarkerSchemaV2, TimelineEventSchema } from "../protocol/v2/index.js";
|
|
4
|
+
import { jsonValue, recoverToolCalls, ToolLifecycleTracker, toolPartial, toolTimelineEvent } from "./tool_lifecycle.js";
|
|
4
5
|
export const TIMELINE_MARKER = "remote-pi:timeline-v2";
|
|
6
|
+
const MAX_PARTIAL_DELTA_CHARS = 64 * 1024;
|
|
5
7
|
export class TimelineRuntime {
|
|
6
8
|
correlations = new AsyncLocalStorage();
|
|
7
9
|
messageCorrelations = new WeakMap();
|
|
@@ -12,17 +14,21 @@ export class TimelineRuntime {
|
|
|
12
14
|
toolResult: [],
|
|
13
15
|
};
|
|
14
16
|
published = [];
|
|
17
|
+
toolAssociations = new ToolLifecycleTracker();
|
|
15
18
|
sessionManager = null;
|
|
16
19
|
epoch = 0;
|
|
20
|
+
stateRevision = 0;
|
|
17
21
|
activeGroupId = null;
|
|
18
22
|
active = false;
|
|
19
23
|
getHistoryGenerationValue;
|
|
20
24
|
onStarted;
|
|
21
25
|
onPublished;
|
|
26
|
+
onPartial;
|
|
22
27
|
constructor(options = {}) {
|
|
23
28
|
this.getHistoryGenerationValue = options.getHistoryGeneration;
|
|
24
29
|
this.onStarted = options.onStarted;
|
|
25
30
|
this.onPublished = options.onPublished;
|
|
31
|
+
this.onPartial = options.onPartial;
|
|
26
32
|
}
|
|
27
33
|
attach(sessionManager) {
|
|
28
34
|
if (this.sessionManager === sessionManager)
|
|
@@ -35,6 +41,7 @@ export class TimelineRuntime {
|
|
|
35
41
|
this.resetState();
|
|
36
42
|
}
|
|
37
43
|
resetState() {
|
|
44
|
+
this.stateRevision += 1;
|
|
38
45
|
this.epoch = 0;
|
|
39
46
|
this.activeGroupId = null;
|
|
40
47
|
this.active = false;
|
|
@@ -42,6 +49,7 @@ export class TimelineRuntime {
|
|
|
42
49
|
this.pendingByRole.user = [];
|
|
43
50
|
this.pendingByRole.assistant = [];
|
|
44
51
|
this.pendingByRole.toolResult = [];
|
|
52
|
+
this.toolAssociations.clear();
|
|
45
53
|
}
|
|
46
54
|
get sessionId() {
|
|
47
55
|
return this.sessionManager?.getSessionId() ?? null;
|
|
@@ -85,6 +93,10 @@ export class TimelineRuntime {
|
|
|
85
93
|
onAgentEnd() {
|
|
86
94
|
this.active = false;
|
|
87
95
|
this.activeGroupId = null;
|
|
96
|
+
this.pendingByRole.user = [];
|
|
97
|
+
this.pendingByRole.assistant = [];
|
|
98
|
+
this.pendingByRole.toolResult = [];
|
|
99
|
+
this.toolAssociations.clear();
|
|
88
100
|
}
|
|
89
101
|
onMessageStart(message, sessionManager) {
|
|
90
102
|
this.attach(sessionManager);
|
|
@@ -102,7 +114,7 @@ export class TimelineRuntime {
|
|
|
102
114
|
sessionManager.appendCustomEntry(TIMELINE_MARKER, marker);
|
|
103
115
|
const objectMessage = message;
|
|
104
116
|
this.messageCorrelations.set(objectMessage, correlation);
|
|
105
|
-
const pending = { message: objectMessage, role: record.role, marker, correlation };
|
|
117
|
+
const pending = { message: objectMessage, role: record.role, marker, correlation, identity: this.messageIdentity(record) };
|
|
106
118
|
this.pending.set(objectMessage, pending);
|
|
107
119
|
this.pendingByRole[record.role].push(pending);
|
|
108
120
|
const started = {
|
|
@@ -115,18 +127,55 @@ export class TimelineRuntime {
|
|
|
115
127
|
this.onStarted?.(started);
|
|
116
128
|
return started;
|
|
117
129
|
}
|
|
130
|
+
onMessageUpdate(event, sessionManager) {
|
|
131
|
+
if (this.sessionManager !== sessionManager || this.messageRole(event.message) !== "assistant")
|
|
132
|
+
return;
|
|
133
|
+
const pending = this.findPending(event.message, "assistant");
|
|
134
|
+
if (!pending)
|
|
135
|
+
return;
|
|
136
|
+
const update = event.assistantMessageEvent;
|
|
137
|
+
if (update.type === "text_start") {
|
|
138
|
+
this.publishPartial(pending, "assistant", update.contentIndex, "running");
|
|
139
|
+
}
|
|
140
|
+
else if (update.type === "thinking_start") {
|
|
141
|
+
this.publishPartial(pending, "thinking", update.contentIndex, "running");
|
|
142
|
+
}
|
|
143
|
+
else if (update.type === "text_delta" && update.delta) {
|
|
144
|
+
this.publishPartial(pending, "assistant", update.contentIndex, "delta", update.delta);
|
|
145
|
+
}
|
|
146
|
+
else if (update.type === "thinking_delta" && update.delta) {
|
|
147
|
+
this.publishPartial(pending, "thinking", update.contentIndex, "delta", update.delta);
|
|
148
|
+
}
|
|
149
|
+
else if (update.type === "toolcall_end") {
|
|
150
|
+
this.toolAssociations.indexAssistantContent([update.toolCall], {
|
|
151
|
+
groupId: pending.marker.group_id,
|
|
152
|
+
correlation: { ...pending.correlation },
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
118
156
|
onMessageEnd(message, sessionManager) {
|
|
119
157
|
this.attach(sessionManager);
|
|
120
158
|
if (typeof message !== "object" || message === null)
|
|
121
159
|
return;
|
|
122
160
|
const objectMessage = message;
|
|
123
|
-
const direct = this.pending.get(objectMessage);
|
|
124
161
|
const role = this.messageRole(message);
|
|
125
|
-
const pending =
|
|
162
|
+
const pending = role ? this.findPending(message, role) : undefined;
|
|
126
163
|
if (!pending)
|
|
127
164
|
return;
|
|
165
|
+
if (pending.role === "assistant") {
|
|
166
|
+
this.toolAssociations.indexAssistantContent(this.asMessageRecord(message)?.content, {
|
|
167
|
+
groupId: pending.marker.group_id,
|
|
168
|
+
correlation: { ...pending.correlation },
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
this.pending.delete(pending.message);
|
|
172
|
+
this.pending.delete(objectMessage);
|
|
128
173
|
this.pendingByRole[pending.role] = this.pendingByRole[pending.role].filter((candidate) => candidate !== pending);
|
|
174
|
+
const revision = this.stateRevision;
|
|
175
|
+
const generation = this.historyGeneration;
|
|
129
176
|
setImmediate(() => {
|
|
177
|
+
if (this.stateRevision !== revision || this.sessionManager !== sessionManager || this.historyGeneration !== generation)
|
|
178
|
+
return;
|
|
130
179
|
const branch = sessionManager.getBranch();
|
|
131
180
|
const markerIndex = branch.findIndex((entry) => entry.id === pending.marker.event_id || (entry.type === "custom" && entry.customType === TIMELINE_MARKER && entry.data &&
|
|
132
181
|
typeof entry.data === "object" && entry.data.event_id === pending.marker.event_id));
|
|
@@ -135,15 +184,50 @@ export class TimelineRuntime {
|
|
|
135
184
|
const target = this.scanTargetAfterMarker(branch, markerIndex, pending.role);
|
|
136
185
|
if (!target)
|
|
137
186
|
return;
|
|
138
|
-
const
|
|
187
|
+
const call = pending.role === "toolResult" ? recoverToolCalls(branch).get(target.id) : undefined;
|
|
188
|
+
const event = this.toTimelineEvent(target, pending.marker, pending.correlation, sessionManager, call);
|
|
139
189
|
if (!event)
|
|
140
190
|
return;
|
|
141
191
|
this.publish(event, pending.correlation);
|
|
192
|
+
if (event.kind === "tool")
|
|
193
|
+
this.toolAssociations.complete(event.tool_call_id);
|
|
142
194
|
});
|
|
143
195
|
}
|
|
196
|
+
onToolExecutionStart(event, sessionManager) {
|
|
197
|
+
if (this.sessionManager !== sessionManager || !this.active)
|
|
198
|
+
return;
|
|
199
|
+
const fallback = this.activeGroupId ? {
|
|
200
|
+
groupId: this.activeGroupId,
|
|
201
|
+
correlation: this.currentCorrelation() ?? { origin: "unknown", delivery: "unknown" },
|
|
202
|
+
} : undefined;
|
|
203
|
+
this.toolAssociations.start(event.toolCallId, event.toolName, event.args, fallback);
|
|
204
|
+
this.publishToolPartial(event.toolCallId);
|
|
205
|
+
}
|
|
206
|
+
onToolExecutionUpdate(event, sessionManager) {
|
|
207
|
+
if (this.sessionManager !== sessionManager || !this.active)
|
|
208
|
+
return;
|
|
209
|
+
this.publishToolPartial(event.toolCallId, event.partialResult);
|
|
210
|
+
}
|
|
211
|
+
onToolExecutionEnd(event, sessionManager) {
|
|
212
|
+
if (this.sessionManager !== sessionManager || !this.active)
|
|
213
|
+
return;
|
|
214
|
+
this.publishToolPartial(event.toolCallId, event.result);
|
|
215
|
+
}
|
|
216
|
+
publishToolPartial(toolCallId, result) {
|
|
217
|
+
const association = this.toolAssociations.get(toolCallId);
|
|
218
|
+
const sessionId = this.sessionId;
|
|
219
|
+
const historyGeneration = this.historyGeneration;
|
|
220
|
+
if (!association || !sessionId || !historyGeneration)
|
|
221
|
+
return;
|
|
222
|
+
const partial = toolPartial(toolCallId, association, { sessionId, historyGeneration }, result);
|
|
223
|
+
if (partial)
|
|
224
|
+
this.onPartial?.(partial, { ...association.correlation });
|
|
225
|
+
}
|
|
144
226
|
recover(sessionManager) {
|
|
145
227
|
this.attach(sessionManager);
|
|
146
228
|
const branch = sessionManager.getBranch();
|
|
229
|
+
const legacyGroup = `legacy:${sessionManager.getSessionId()}`;
|
|
230
|
+
const recoveryTools = recoverToolCalls(branch);
|
|
147
231
|
const matched = new Set();
|
|
148
232
|
const recovered = [];
|
|
149
233
|
for (let index = 0; index < branch.length; index += 1) {
|
|
@@ -156,12 +240,11 @@ export class TimelineRuntime {
|
|
|
156
240
|
if (!target || target.type !== "message")
|
|
157
241
|
continue;
|
|
158
242
|
matched.add(target.id);
|
|
159
|
-
const event = this.toTimelineEvent(target, marker, this.correlationFromMarker(marker), sessionManager);
|
|
243
|
+
const event = this.toTimelineEvent(target, marker, this.correlationFromMarker(marker), sessionManager, recoveryTools.get(target.id));
|
|
160
244
|
if (event)
|
|
161
245
|
recovered.push(event);
|
|
162
246
|
}
|
|
163
247
|
}
|
|
164
|
-
const legacyGroup = `legacy:${sessionManager.getSessionId()}`;
|
|
165
248
|
for (const entry of branch) {
|
|
166
249
|
if (entry.type !== "message" || matched.has(entry.id))
|
|
167
250
|
continue;
|
|
@@ -171,7 +254,7 @@ export class TimelineRuntime {
|
|
|
171
254
|
const marker = role === "user"
|
|
172
255
|
? { version: 2, event_id: `legacy:${entry.id}`, group_id: legacyGroup, kind: "user", origin: "unknown", delivery: "unknown" }
|
|
173
256
|
: { version: 2, event_id: `legacy:${entry.id}`, group_id: legacyGroup, kind: role === "assistant" ? "assistant" : "tool" };
|
|
174
|
-
const event = this.toTimelineEvent(entry, marker, this.correlationFromMarker(marker), sessionManager);
|
|
257
|
+
const event = this.toTimelineEvent(entry, marker, this.correlationFromMarker(marker), sessionManager, recoveryTools.get(entry.id));
|
|
175
258
|
if (event)
|
|
176
259
|
recovered.push(event);
|
|
177
260
|
}
|
|
@@ -182,12 +265,69 @@ export class TimelineRuntime {
|
|
|
182
265
|
}
|
|
183
266
|
return recovered;
|
|
184
267
|
}
|
|
268
|
+
findPending(message, role) {
|
|
269
|
+
if (typeof message !== "object" || message === null)
|
|
270
|
+
return undefined;
|
|
271
|
+
const direct = this.pending.get(message);
|
|
272
|
+
if (direct?.role === role && this.pendingByRole[role].includes(direct))
|
|
273
|
+
return direct;
|
|
274
|
+
const record = this.asMessageRecord(message);
|
|
275
|
+
const identity = record ? this.messageIdentity(record) : null;
|
|
276
|
+
if (!identity)
|
|
277
|
+
return undefined;
|
|
278
|
+
return this.pendingByRole[role].find((candidate) => candidate.identity === identity);
|
|
279
|
+
}
|
|
280
|
+
messageIdentity(message) {
|
|
281
|
+
if (typeof message.timestamp !== "number" || !Number.isFinite(message.timestamp))
|
|
282
|
+
return null;
|
|
283
|
+
return [message.role, message.timestamp, message.api ?? "", message.provider ?? "", message.model ?? ""].join(":");
|
|
284
|
+
}
|
|
185
285
|
publish(event, correlation) {
|
|
186
286
|
if (this.published.some((existing) => existing.event_id === event.event_id))
|
|
187
287
|
return;
|
|
188
288
|
this.published.push(event);
|
|
189
289
|
this.onPublished?.(event, { ...correlation });
|
|
190
290
|
}
|
|
291
|
+
publishPartial(pending, kind, contentIndex, status, delta) {
|
|
292
|
+
const sessionId = this.sessionId;
|
|
293
|
+
const historyGeneration = this.historyGeneration;
|
|
294
|
+
const groupId = pending.marker.group_id;
|
|
295
|
+
if (!sessionId || !historyGeneration || !groupId)
|
|
296
|
+
return;
|
|
297
|
+
const publish = (chunk) => {
|
|
298
|
+
const partial = {
|
|
299
|
+
protocol_version: 2,
|
|
300
|
+
type: "timeline_partial",
|
|
301
|
+
session_id: sessionId,
|
|
302
|
+
history_generation: historyGeneration,
|
|
303
|
+
group_id: groupId,
|
|
304
|
+
partial_id: `${pending.marker.event_id}:${kind}:${contentIndex}`,
|
|
305
|
+
kind,
|
|
306
|
+
status,
|
|
307
|
+
...(chunk === undefined ? {} : { delta: chunk }),
|
|
308
|
+
};
|
|
309
|
+
this.onPartial?.(partial, { ...pending.correlation });
|
|
310
|
+
};
|
|
311
|
+
if (delta === undefined) {
|
|
312
|
+
publish();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
for (const chunk of this.partialDeltaChunks(delta))
|
|
316
|
+
publish(chunk);
|
|
317
|
+
}
|
|
318
|
+
partialDeltaChunks(delta) {
|
|
319
|
+
const chunks = [];
|
|
320
|
+
for (let offset = 0; offset < delta.length;) {
|
|
321
|
+
let end = Math.min(delta.length, offset + MAX_PARTIAL_DELTA_CHARS);
|
|
322
|
+
const finalCodeUnit = delta.charCodeAt(end - 1);
|
|
323
|
+
const nextCodeUnit = delta.charCodeAt(end);
|
|
324
|
+
if (end < delta.length && finalCodeUnit >= 0xD800 && finalCodeUnit <= 0xDBFF && nextCodeUnit >= 0xDC00 && nextCodeUnit <= 0xDFFF)
|
|
325
|
+
end -= 1;
|
|
326
|
+
chunks.push(delta.slice(offset, end));
|
|
327
|
+
offset = end;
|
|
328
|
+
}
|
|
329
|
+
return chunks;
|
|
330
|
+
}
|
|
191
331
|
correlationFor(message) {
|
|
192
332
|
const existing = this.currentCorrelation();
|
|
193
333
|
if (existing)
|
|
@@ -239,7 +379,7 @@ export class TimelineRuntime {
|
|
|
239
379
|
}
|
|
240
380
|
return undefined;
|
|
241
381
|
}
|
|
242
|
-
toTimelineEvent(entry, marker, correlation, sessionManager) {
|
|
382
|
+
toTimelineEvent(entry, marker, correlation, sessionManager, association) {
|
|
243
383
|
const message = this.asMessageRecord(entry.message);
|
|
244
384
|
if (!message)
|
|
245
385
|
return null;
|
|
@@ -248,6 +388,8 @@ export class TimelineRuntime {
|
|
|
248
388
|
const historyGeneration = this.getHistoryGenerationValue?.() ?? sessionId;
|
|
249
389
|
const base = { event_id: marker.event_id, session_id: sessionId, history_generation: historyGeneration, timestamp };
|
|
250
390
|
const groupId = marker.group_id;
|
|
391
|
+
if (!groupId)
|
|
392
|
+
return null;
|
|
251
393
|
if (message.role === "user") {
|
|
252
394
|
const senderRef = marker.kind === "user" && marker.sender_ref ? { sender_ref: marker.sender_ref } : {};
|
|
253
395
|
return TimelineEventSchema.parse({
|
|
@@ -270,19 +412,7 @@ export class TimelineRuntime {
|
|
|
270
412
|
status: message.stopReason === "aborted" ? "interrupted" : "complete",
|
|
271
413
|
});
|
|
272
414
|
}
|
|
273
|
-
|
|
274
|
-
if (message.isError) {
|
|
275
|
-
return TimelineEventSchema.parse({
|
|
276
|
-
...base, group_id: groupId, kind: "tool", tool_call_id: message.toolCallId ?? marker.event_id,
|
|
277
|
-
tool: message.toolName ?? "unknown", args: this.jsonValue(message.args ?? {}), truncated: false,
|
|
278
|
-
status: "error", error: this.nonEmpty(message.errorMessage ?? this.textFromContent(message.content) ?? "tool failed"),
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
return TimelineEventSchema.parse({
|
|
282
|
-
...base, group_id: groupId, kind: "tool", tool_call_id: message.toolCallId ?? marker.event_id,
|
|
283
|
-
tool: message.toolName ?? "unknown", args: this.jsonValue(message.args ?? {}), truncated: false,
|
|
284
|
-
status: "complete", result,
|
|
285
|
-
});
|
|
415
|
+
return toolTimelineEvent(base, groupId, message, association);
|
|
286
416
|
}
|
|
287
417
|
toSystemEvent(entry, sessionManager) {
|
|
288
418
|
const base = {
|
|
@@ -302,7 +432,7 @@ export class TimelineRuntime {
|
|
|
302
432
|
first_kept_entry_id: entry.firstKeptEntryId,
|
|
303
433
|
tokens_before: entry.tokensBefore,
|
|
304
434
|
from_hook: entry.fromHook ?? false,
|
|
305
|
-
...(entry.details === undefined ? {} : { details:
|
|
435
|
+
...(entry.details === undefined ? {} : { details: jsonValue(entry.details) }),
|
|
306
436
|
},
|
|
307
437
|
};
|
|
308
438
|
}
|
|
@@ -314,7 +444,7 @@ export class TimelineRuntime {
|
|
|
314
444
|
summary: entry.summary,
|
|
315
445
|
from_id: entry.fromId,
|
|
316
446
|
from_hook: entry.fromHook ?? false,
|
|
317
|
-
...(entry.details === undefined ? {} : { details:
|
|
447
|
+
...(entry.details === undefined ? {} : { details: jsonValue(entry.details) }),
|
|
318
448
|
},
|
|
319
449
|
};
|
|
320
450
|
}
|
|
@@ -324,7 +454,7 @@ export class TimelineRuntime {
|
|
|
324
454
|
kind: "custom",
|
|
325
455
|
payload: {
|
|
326
456
|
custom_type: entry.customType,
|
|
327
|
-
...(entry.data === undefined ? {} : { data:
|
|
457
|
+
...(entry.data === undefined ? {} : { data: jsonValue(entry.data) }),
|
|
328
458
|
},
|
|
329
459
|
};
|
|
330
460
|
}
|
|
@@ -380,9 +510,10 @@ export class TimelineRuntime {
|
|
|
380
510
|
if (!part || typeof part !== "object")
|
|
381
511
|
return [];
|
|
382
512
|
const item = part;
|
|
383
|
-
if (
|
|
384
|
-
return [{ type:
|
|
385
|
-
|
|
513
|
+
if (item.type === "text" && typeof item.text === "string")
|
|
514
|
+
return [{ type: "text", text: item.text }];
|
|
515
|
+
if (item.type === "thinking" && typeof item.thinking === "string")
|
|
516
|
+
return [{ type: "thinking", text: item.thinking }];
|
|
386
517
|
return [];
|
|
387
518
|
});
|
|
388
519
|
}
|
|
@@ -409,20 +540,5 @@ export class TimelineRuntime {
|
|
|
409
540
|
return 0;
|
|
410
541
|
}
|
|
411
542
|
}
|
|
412
|
-
jsonValue(value) {
|
|
413
|
-
if (value === null || typeof value === "boolean" || typeof value === "string")
|
|
414
|
-
return value;
|
|
415
|
-
if (typeof value === "number")
|
|
416
|
-
return Number.isFinite(value) ? value : null;
|
|
417
|
-
if (Array.isArray(value))
|
|
418
|
-
return value.map((item) => this.jsonValue(item));
|
|
419
|
-
if (typeof value === "object") {
|
|
420
|
-
const result = {};
|
|
421
|
-
for (const [key, item] of Object.entries(value))
|
|
422
|
-
result[key] = this.jsonValue(item);
|
|
423
|
-
return result;
|
|
424
|
-
}
|
|
425
|
-
return null;
|
|
426
|
-
}
|
|
427
543
|
}
|
|
428
544
|
//# sourceMappingURL=runtime.js.map
|