@kb-labs/agent-core 2.118.2 → 2.119.0-canary.2077501f1
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.d.ts +76 -21
- package/dist/index.js +217 -50
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _kb_labs_agent_contracts from '@kb-labs/agent-contracts';
|
|
2
|
-
import { AgentMode, AgentSessionInfo, AgentSession, AgentEvent, Turn, FileChangeSummary, AgentEventCallback, Tracer, LLMTier, TaskPlan, SessionProgress, TaskSpec, AgentConfig, TaskResult, ModeConfig, SpecSection, StopConditionResult, LoopResult, AgentMemory, FeatureFlags, SpawnAgentRequest, SpawnAgentResult, AsyncTask, ToolPack, ToolFilter, ToolDefinition, ResolvedTool, ToolResult, IterationSnapshot, RunEvaluation } from '@kb-labs/agent-contracts';
|
|
2
|
+
import { AgentMode, AgentSessionInfo, AgentSession, AgentEvent, TurnDelta, Turn, FileChangeSummary, AgentEventCallback, Tracer, LLMTier, TaskPlan, SessionProgress, TaskSpec, AgentConfig, TaskResult, ModeConfig, SpecSection, StopConditionResult, LoopResult, AgentMemory, FeatureFlags, SpawnAgentRequest, SpawnAgentResult, AsyncTask, ToolPack, ToolFilter, ToolDefinition, ResolvedTool, ToolResult, IterationSnapshot, RunEvaluation } from '@kb-labs/agent-contracts';
|
|
3
3
|
import { ToolRegistry, SessionMemoryBridge } from '@kb-labs/agent-tools';
|
|
4
4
|
import { RuntimeProfile, ResultMapper, StopCondition, RunContext, LLMCallResult, ExecutionLoop as ExecutionLoop$1, LoopContext as LoopContext$1, LoopResult as LoopResult$1, AgentMiddleware, ControlAction, LLMCtx, LLMCallPatch, ToolExecCtx, ToolOutput, IAgentRunner, AgentSDK, ToolGuard, OutputProcessor, InputNormalizer, ToolCallInput, ContextMeta } from '@kb-labs/agent-sdk';
|
|
5
5
|
export { AgentMiddleware, ControlAction, LLMCallPatch, LLMCallResult, LLMCtx, RunContext, ToolExecCtx, ToolOutput } from '@kb-labs/agent-sdk';
|
|
@@ -40,21 +40,27 @@ interface SessionKpiBaseline {
|
|
|
40
40
|
declare class SessionManager {
|
|
41
41
|
private workingDir;
|
|
42
42
|
private readonly artifactStore;
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
/**
|
|
44
|
+
* In-memory cache of the per-session monotonic event sequence counter
|
|
45
|
+
* (initialized lazily from NDJSON line count). Backed by shared
|
|
46
|
+
* process-wide state — see SharedSessionManagerState's doc comment for
|
|
47
|
+
* why this must NOT be a plain instance field.
|
|
48
|
+
*/
|
|
49
|
+
private get sessionSeqCounters();
|
|
50
|
+
/** In-memory cache of turn sequence counters per session (for turns.json). Shared — see sessionSeqCounters. */
|
|
51
|
+
private get turnSeqCounters();
|
|
47
52
|
/** Turn assembler for creating turn snapshots from events */
|
|
48
53
|
private assembler;
|
|
49
|
-
/** Write queue per session to prevent concurrent write race conditions */
|
|
50
|
-
private writeQueues;
|
|
54
|
+
/** Write queue per session to prevent concurrent write race conditions. Shared — see sessionSeqCounters. */
|
|
55
|
+
private get writeQueues();
|
|
51
56
|
/**
|
|
52
57
|
* Serialized event-processing queue per session.
|
|
53
58
|
* Ensures addEvent calls for the same session are processed in arrival order
|
|
54
59
|
* even when the caller fires them concurrently with void (fire-and-forget).
|
|
55
60
|
* This prevents tool:end being processed by TurnAssembler before tool:start.
|
|
61
|
+
* Shared — see sessionSeqCounters.
|
|
56
62
|
*/
|
|
57
|
-
private eventQueues;
|
|
63
|
+
private get eventQueues();
|
|
58
64
|
/** Write queue per session for artifact projection updates */
|
|
59
65
|
private artifactWriteQueues;
|
|
60
66
|
/** Write queue per session for KPI baseline updates */
|
|
@@ -108,22 +114,41 @@ declare class SessionManager {
|
|
|
108
114
|
total: number;
|
|
109
115
|
}>;
|
|
110
116
|
/**
|
|
111
|
-
* Get next
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
|
|
117
|
+
* Get the next seq for a session — monotonic across the WHOLE session
|
|
118
|
+
* (every run, every turn), not scoped to a single run. This is the one
|
|
119
|
+
* cursor the WS protocol and REST resume/gap-recovery paths rely on;
|
|
120
|
+
* it must be a single, ever-increasing number per session for "seq > N"
|
|
121
|
+
* comparisons to mean anything.
|
|
122
|
+
*
|
|
123
|
+
* Lazily initialized from the NDJSON LINE COUNT, not the max embedded
|
|
124
|
+
* `sessionSeq` value. Old sessions were written under the previous
|
|
125
|
+
* per-run scheme, where two different runs both start their own
|
|
126
|
+
* `sessionSeq` at 1 — so the max embedded value can be far smaller than
|
|
127
|
+
* the true number of events and would hand out colliding seq numbers
|
|
128
|
+
* for old sessions. Line count is always >= any embedded per-run value
|
|
129
|
+
* and is exactly equal to it for sessions already written under this
|
|
130
|
+
* per-session scheme, so it's a safe bootstrap either way.
|
|
131
|
+
*/
|
|
132
|
+
private ensureSessionSeqBootstrapped;
|
|
116
133
|
private getNextSessionSeq;
|
|
134
|
+
/**
|
|
135
|
+
* Current session-wide cursor value WITHOUT advancing it. Used for the
|
|
136
|
+
* cold-start snapshot and gap-recovery projection, both of which need to
|
|
137
|
+
* hand the client a `seq` to resume from without consuming one.
|
|
138
|
+
*/
|
|
139
|
+
getCurrentSessionSeq(sessionId: string): Promise<number>;
|
|
117
140
|
/**
|
|
118
141
|
* Add event to session (append-only, race-condition safe).
|
|
119
|
-
* Assigns
|
|
120
|
-
* Also processes event to update turn snapshots
|
|
142
|
+
* Assigns the session-wide monotonic sessionSeq.
|
|
143
|
+
* Also processes the event to update turn snapshots, returning whatever
|
|
144
|
+
* TurnDelta[] that produced (empty if the event didn't touch a turn) so
|
|
145
|
+
* callers can broadcast them once persistence is confirmed.
|
|
121
146
|
*
|
|
122
147
|
* Events for the same session are serialized via eventQueues so that
|
|
123
148
|
* concurrent fire-and-forget callers (void addEvent(...)) don't cause
|
|
124
149
|
* tool:end to be processed before tool:start in TurnAssembler.
|
|
125
150
|
*/
|
|
126
|
-
addEvent(sessionId: string, event: AgentEvent): Promise<
|
|
151
|
+
addEvent(sessionId: string, event: AgentEvent): Promise<TurnDelta[]>;
|
|
127
152
|
private _addEventInternal;
|
|
128
153
|
/**
|
|
129
154
|
* Get all events for a session
|
|
@@ -222,17 +247,47 @@ declare class SessionManager {
|
|
|
222
247
|
* Create user turn with task/question
|
|
223
248
|
* Called when user submits a task to agent
|
|
224
249
|
*/
|
|
225
|
-
createUserTurn(sessionId: string, task: string, runId: string): Promise<Turn>;
|
|
250
|
+
createUserTurn(sessionId: string, task: string, runId: string, clientId?: string): Promise<Turn>;
|
|
226
251
|
/**
|
|
227
252
|
* Reserve next turn sequence number atomically per session.
|
|
228
253
|
* Uses the same serialized write queue as turns.json updates.
|
|
229
254
|
*/
|
|
230
255
|
private reserveNextTurnSequence;
|
|
231
256
|
/**
|
|
232
|
-
* Process event
|
|
233
|
-
*
|
|
234
|
-
|
|
235
|
-
|
|
257
|
+
* Process event, update the turn projection, and compute the deltas this
|
|
258
|
+
* mutation produced (relative to the turn's state just before this
|
|
259
|
+
* event). Returns null when the event didn't touch any turn, or touched
|
|
260
|
+
* one without producing any externally-observable change.
|
|
261
|
+
*
|
|
262
|
+
* The "before" snapshot is captured from the assembler's live (in-place
|
|
263
|
+
* mutated) turn reference BEFORE processEventAsync runs — the assembler
|
|
264
|
+
* mutates the same object, so this is the only point at which a
|
|
265
|
+
* pre-mutation snapshot is available.
|
|
266
|
+
*
|
|
267
|
+
* Deliberately resolves the CURRENT turn via peekTurn rather than trusting
|
|
268
|
+
* processEventAsync's own return value: the assembler treats "just
|
|
269
|
+
* created, no steps yet" (a bare agent:start with nothing else to report)
|
|
270
|
+
* as "not updated" and returns null for it — but the turn WAS created as
|
|
271
|
+
* a side effect (added to activeTurns), which is itself diff-worthy: it's
|
|
272
|
+
* the only point `turn:created` can ever fire from. Without this, the
|
|
273
|
+
* turn is silently created in memory but never persisted or broadcast,
|
|
274
|
+
* and every later event sees `before !== null` (peekTurn already finds
|
|
275
|
+
* it), so `turn:created` never fires at all.
|
|
276
|
+
*/
|
|
277
|
+
processEventAndUpdateTurn(sessionId: string, event: AgentEvent): Promise<{
|
|
278
|
+
turn: Turn;
|
|
279
|
+
deltas: TurnDelta[];
|
|
280
|
+
} | null>;
|
|
281
|
+
/**
|
|
282
|
+
* Current projection of a session: its full turn list plus the seq cursor
|
|
283
|
+
* it reflects. Used for the cold-start snapshot AND as the gap-recovery
|
|
284
|
+
* resume payload (see ADR note in the sync design doc) — both send the
|
|
285
|
+
* same shape, just at different points in the connection lifecycle.
|
|
286
|
+
*/
|
|
287
|
+
getProjection(sessionId: string): Promise<{
|
|
288
|
+
turns: Turn[];
|
|
289
|
+
seq: number;
|
|
290
|
+
}>;
|
|
236
291
|
/**
|
|
237
292
|
* Store extracted trace artifacts for a completed run.
|
|
238
293
|
*/
|
package/dist/index.js
CHANGED
|
@@ -101,6 +101,24 @@ var init_turn_assembler = __esm({
|
|
|
101
101
|
}
|
|
102
102
|
return updated || completed ? turn : null;
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Public wrapper around getTurnId, for callers that need to snapshot the
|
|
106
|
+
* turn's pre-mutation state (e.g. to diff against post-mutation state)
|
|
107
|
+
* before calling processEventAsync.
|
|
108
|
+
*/
|
|
109
|
+
getTurnIdForEvent(event) {
|
|
110
|
+
return this.getTurnId(event);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Peek at the current in-progress turn for a turn id, WITHOUT mutating
|
|
114
|
+
* anything. Returns a live reference (not a clone) — the assembler
|
|
115
|
+
* mutates turn/step objects in place, so callers that need a stable
|
|
116
|
+
* "before" snapshot must clone this themselves before calling
|
|
117
|
+
* processEventAsync.
|
|
118
|
+
*/
|
|
119
|
+
peekTurn(turnId) {
|
|
120
|
+
return this.activeTurns.get(turnId);
|
|
121
|
+
}
|
|
104
122
|
/**
|
|
105
123
|
* Derive turn ID from event.
|
|
106
124
|
* For root agents: use agentId
|
|
@@ -486,33 +504,124 @@ var init_turn_assembler = __esm({
|
|
|
486
504
|
}
|
|
487
505
|
});
|
|
488
506
|
|
|
507
|
+
// src/planning/turn-diff.ts
|
|
508
|
+
function stepsEqual(a, b) {
|
|
509
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
510
|
+
}
|
|
511
|
+
function diffTurn(before, after, seq) {
|
|
512
|
+
const deltas = [];
|
|
513
|
+
if (!before) {
|
|
514
|
+
deltas.push({ kind: "turn:created", seq, turn: { ...after, steps: [] } });
|
|
515
|
+
for (const step of after.steps) {
|
|
516
|
+
deltas.push({ kind: "turn:step:appended", seq, turnId: after.id, step });
|
|
517
|
+
}
|
|
518
|
+
if (after.status !== "streaming") {
|
|
519
|
+
deltas.push({
|
|
520
|
+
kind: "turn:status",
|
|
521
|
+
seq,
|
|
522
|
+
turnId: after.id,
|
|
523
|
+
status: after.status,
|
|
524
|
+
completedAt: after.completedAt,
|
|
525
|
+
error: after.error
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
return deltas;
|
|
529
|
+
}
|
|
530
|
+
for (let i = 0; i < after.steps.length; i++) {
|
|
531
|
+
const afterStep = after.steps[i];
|
|
532
|
+
if (!afterStep) {
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
const beforeStep = before.steps[i];
|
|
536
|
+
if (!beforeStep) {
|
|
537
|
+
deltas.push({ kind: "turn:step:appended", seq, turnId: after.id, step: afterStep });
|
|
538
|
+
} else if (!stepsEqual(beforeStep, afterStep)) {
|
|
539
|
+
deltas.push({ kind: "turn:step:updated", seq, turnId: after.id, step: afterStep });
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (before.status !== after.status || before.completedAt !== after.completedAt) {
|
|
543
|
+
deltas.push({
|
|
544
|
+
kind: "turn:status",
|
|
545
|
+
seq,
|
|
546
|
+
turnId: after.id,
|
|
547
|
+
status: after.status,
|
|
548
|
+
completedAt: after.completedAt,
|
|
549
|
+
error: after.error
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
const beforeMeta = before.metadata;
|
|
553
|
+
const afterMeta = after.metadata;
|
|
554
|
+
const metaPatch = {};
|
|
555
|
+
for (const key of Object.keys(afterMeta)) {
|
|
556
|
+
if (!stepsEqual(beforeMeta[key], afterMeta[key])) {
|
|
557
|
+
metaPatch[key] = afterMeta[key];
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (Object.keys(metaPatch).length > 0) {
|
|
561
|
+
deltas.push({ kind: "turn:metadata", seq, turnId: after.id, patch: metaPatch });
|
|
562
|
+
}
|
|
563
|
+
return deltas;
|
|
564
|
+
}
|
|
565
|
+
var init_turn_diff = __esm({
|
|
566
|
+
"src/planning/turn-diff.ts"() {
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
|
|
489
570
|
// src/planning/session-manager.ts
|
|
490
571
|
var session_manager_exports = {};
|
|
491
572
|
__export(session_manager_exports, {
|
|
492
573
|
SessionManager: () => SessionManager
|
|
493
574
|
});
|
|
494
|
-
|
|
575
|
+
function getSharedState() {
|
|
576
|
+
const g = globalThis;
|
|
577
|
+
if (!g[SHARED_STATE_KEY]) {
|
|
578
|
+
g[SHARED_STATE_KEY] = {
|
|
579
|
+
sessionSeqCounters: /* @__PURE__ */ new Map(),
|
|
580
|
+
turnSeqCounters: /* @__PURE__ */ new Map(),
|
|
581
|
+
writeQueues: /* @__PURE__ */ new Map(),
|
|
582
|
+
eventQueues: /* @__PURE__ */ new Map()
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
return g[SHARED_STATE_KEY];
|
|
586
|
+
}
|
|
587
|
+
var SHARED_STATE_KEY, SessionManager;
|
|
495
588
|
var init_session_manager = __esm({
|
|
496
589
|
"src/planning/session-manager.ts"() {
|
|
497
590
|
init_turn_assembler();
|
|
591
|
+
init_turn_diff();
|
|
592
|
+
SHARED_STATE_KEY = "__kb_agent_session_manager_shared_state__";
|
|
498
593
|
SessionManager = class {
|
|
499
594
|
workingDir;
|
|
500
595
|
artifactStore;
|
|
501
|
-
/**
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
596
|
+
/**
|
|
597
|
+
* In-memory cache of the per-session monotonic event sequence counter
|
|
598
|
+
* (initialized lazily from NDJSON line count). Backed by shared
|
|
599
|
+
* process-wide state — see SharedSessionManagerState's doc comment for
|
|
600
|
+
* why this must NOT be a plain instance field.
|
|
601
|
+
*/
|
|
602
|
+
get sessionSeqCounters() {
|
|
603
|
+
return getSharedState().sessionSeqCounters;
|
|
604
|
+
}
|
|
605
|
+
/** In-memory cache of turn sequence counters per session (for turns.json). Shared — see sessionSeqCounters. */
|
|
606
|
+
get turnSeqCounters() {
|
|
607
|
+
return getSharedState().turnSeqCounters;
|
|
608
|
+
}
|
|
505
609
|
/** Turn assembler for creating turn snapshots from events */
|
|
506
610
|
assembler = new TurnAssembler();
|
|
507
|
-
/** Write queue per session to prevent concurrent write race conditions */
|
|
508
|
-
writeQueues
|
|
611
|
+
/** Write queue per session to prevent concurrent write race conditions. Shared — see sessionSeqCounters. */
|
|
612
|
+
get writeQueues() {
|
|
613
|
+
return getSharedState().writeQueues;
|
|
614
|
+
}
|
|
509
615
|
/**
|
|
510
616
|
* Serialized event-processing queue per session.
|
|
511
617
|
* Ensures addEvent calls for the same session are processed in arrival order
|
|
512
618
|
* even when the caller fires them concurrently with void (fire-and-forget).
|
|
513
619
|
* This prevents tool:end being processed by TurnAssembler before tool:start.
|
|
620
|
+
* Shared — see sessionSeqCounters.
|
|
514
621
|
*/
|
|
515
|
-
eventQueues
|
|
622
|
+
get eventQueues() {
|
|
623
|
+
return getSharedState().eventQueues;
|
|
624
|
+
}
|
|
516
625
|
/** Write queue per session for artifact projection updates */
|
|
517
626
|
artifactWriteQueues = /* @__PURE__ */ new Map();
|
|
518
627
|
/** Write queue per session for KPI baseline updates */
|
|
@@ -657,39 +766,54 @@ var init_session_manager = __esm({
|
|
|
657
766
|
// Event Storage (NDJSON format - append-safe)
|
|
658
767
|
// ═══════════════════════════════════════════════════════════════════════
|
|
659
768
|
/**
|
|
660
|
-
* Get next
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
769
|
+
* Get the next seq for a session — monotonic across the WHOLE session
|
|
770
|
+
* (every run, every turn), not scoped to a single run. This is the one
|
|
771
|
+
* cursor the WS protocol and REST resume/gap-recovery paths rely on;
|
|
772
|
+
* it must be a single, ever-increasing number per session for "seq > N"
|
|
773
|
+
* comparisons to mean anything.
|
|
774
|
+
*
|
|
775
|
+
* Lazily initialized from the NDJSON LINE COUNT, not the max embedded
|
|
776
|
+
* `sessionSeq` value. Old sessions were written under the previous
|
|
777
|
+
* per-run scheme, where two different runs both start their own
|
|
778
|
+
* `sessionSeq` at 1 — so the max embedded value can be far smaller than
|
|
779
|
+
* the true number of events and would hand out colliding seq numbers
|
|
780
|
+
* for old sessions. Line count is always >= any embedded per-run value
|
|
781
|
+
* and is exactly equal to it for sessions already written under this
|
|
782
|
+
* per-session scheme, so it's a safe bootstrap either way.
|
|
664
783
|
*/
|
|
665
|
-
async
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
const evt = JSON.parse(line);
|
|
675
|
-
if (evt.runId === runId && evt.sessionSeq != null && evt.sessionSeq > maxSeq) {
|
|
676
|
-
maxSeq = evt.sessionSeq;
|
|
677
|
-
}
|
|
678
|
-
} catch {
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
} catch {
|
|
682
|
-
}
|
|
683
|
-
this.runSeqCounters.set(key, maxSeq);
|
|
784
|
+
async ensureSessionSeqBootstrapped(sessionId) {
|
|
785
|
+
if (this.sessionSeqCounters.has(sessionId)) {
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
let lineCount = 0;
|
|
789
|
+
try {
|
|
790
|
+
const content = await promises.readFile(this.getEventsPath(sessionId), "utf-8");
|
|
791
|
+
lineCount = content.split("\n").filter((l) => l.trim()).length;
|
|
792
|
+
} catch {
|
|
684
793
|
}
|
|
685
|
-
|
|
686
|
-
|
|
794
|
+
this.sessionSeqCounters.set(sessionId, lineCount);
|
|
795
|
+
}
|
|
796
|
+
async getNextSessionSeq(sessionId) {
|
|
797
|
+
await this.ensureSessionSeqBootstrapped(sessionId);
|
|
798
|
+
const next = this.sessionSeqCounters.get(sessionId) + 1;
|
|
799
|
+
this.sessionSeqCounters.set(sessionId, next);
|
|
687
800
|
return next;
|
|
688
801
|
}
|
|
802
|
+
/**
|
|
803
|
+
* Current session-wide cursor value WITHOUT advancing it. Used for the
|
|
804
|
+
* cold-start snapshot and gap-recovery projection, both of which need to
|
|
805
|
+
* hand the client a `seq` to resume from without consuming one.
|
|
806
|
+
*/
|
|
807
|
+
async getCurrentSessionSeq(sessionId) {
|
|
808
|
+
await this.ensureSessionSeqBootstrapped(sessionId);
|
|
809
|
+
return this.sessionSeqCounters.get(sessionId);
|
|
810
|
+
}
|
|
689
811
|
/**
|
|
690
812
|
* Add event to session (append-only, race-condition safe).
|
|
691
|
-
* Assigns
|
|
692
|
-
* Also processes event to update turn snapshots
|
|
813
|
+
* Assigns the session-wide monotonic sessionSeq.
|
|
814
|
+
* Also processes the event to update turn snapshots, returning whatever
|
|
815
|
+
* TurnDelta[] that produced (empty if the event didn't touch a turn) so
|
|
816
|
+
* callers can broadcast them once persistence is confirmed.
|
|
693
817
|
*
|
|
694
818
|
* Events for the same session are serialized via eventQueues so that
|
|
695
819
|
* concurrent fire-and-forget callers (void addEvent(...)) don't cause
|
|
@@ -708,12 +832,12 @@ var init_session_manager = __esm({
|
|
|
708
832
|
const sessionDir = this.getSessionDir(sessionId);
|
|
709
833
|
await promises.mkdir(sessionDir, { recursive: true });
|
|
710
834
|
await this.artifactStore.ensureSessionArtifacts(sessionId);
|
|
711
|
-
const
|
|
712
|
-
const sessionSeq = await this.getNextSessionSeq(sessionId, runId);
|
|
835
|
+
const sessionSeq = await this.getNextSessionSeq(sessionId);
|
|
713
836
|
const line = JSON.stringify({ ...event, sessionSeq }) + "\n";
|
|
714
837
|
await promises.appendFile(this.getEventsPath(sessionId), line, "utf-8");
|
|
715
838
|
await promises.appendFile(this.artifactStore.getArtifactPath(sessionId, "trace.ndjson"), line, "utf-8");
|
|
716
|
-
await this.processEventAndUpdateTurn(sessionId, { ...event, sessionSeq });
|
|
839
|
+
const result = await this.processEventAndUpdateTurn(sessionId, { ...event, sessionSeq });
|
|
840
|
+
return result?.deltas ?? [];
|
|
717
841
|
}
|
|
718
842
|
/**
|
|
719
843
|
* Get all events for a session
|
|
@@ -1104,7 +1228,7 @@ ${tail}`;
|
|
|
1104
1228
|
* Create user turn with task/question
|
|
1105
1229
|
* Called when user submits a task to agent
|
|
1106
1230
|
*/
|
|
1107
|
-
async createUserTurn(sessionId, task, runId) {
|
|
1231
|
+
async createUserTurn(sessionId, task, runId, clientId) {
|
|
1108
1232
|
const sequence = await this.reserveNextTurnSequence(sessionId);
|
|
1109
1233
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1110
1234
|
const userTurn = {
|
|
@@ -1125,7 +1249,8 @@ ${tail}`;
|
|
|
1125
1249
|
}
|
|
1126
1250
|
],
|
|
1127
1251
|
metadata: {
|
|
1128
|
-
agentId: "user"
|
|
1252
|
+
agentId: "user",
|
|
1253
|
+
...clientId ? { clientId } : {}
|
|
1129
1254
|
}
|
|
1130
1255
|
};
|
|
1131
1256
|
await this.storeTurnSnapshot(sessionId, userTurn);
|
|
@@ -1153,21 +1278,63 @@ ${tail}`;
|
|
|
1153
1278
|
return reserved;
|
|
1154
1279
|
}
|
|
1155
1280
|
/**
|
|
1156
|
-
* Process event
|
|
1157
|
-
*
|
|
1281
|
+
* Process event, update the turn projection, and compute the deltas this
|
|
1282
|
+
* mutation produced (relative to the turn's state just before this
|
|
1283
|
+
* event). Returns null when the event didn't touch any turn, or touched
|
|
1284
|
+
* one without producing any externally-observable change.
|
|
1285
|
+
*
|
|
1286
|
+
* The "before" snapshot is captured from the assembler's live (in-place
|
|
1287
|
+
* mutated) turn reference BEFORE processEventAsync runs — the assembler
|
|
1288
|
+
* mutates the same object, so this is the only point at which a
|
|
1289
|
+
* pre-mutation snapshot is available.
|
|
1290
|
+
*
|
|
1291
|
+
* Deliberately resolves the CURRENT turn via peekTurn rather than trusting
|
|
1292
|
+
* processEventAsync's own return value: the assembler treats "just
|
|
1293
|
+
* created, no steps yet" (a bare agent:start with nothing else to report)
|
|
1294
|
+
* as "not updated" and returns null for it — but the turn WAS created as
|
|
1295
|
+
* a side effect (added to activeTurns), which is itself diff-worthy: it's
|
|
1296
|
+
* the only point `turn:created` can ever fire from. Without this, the
|
|
1297
|
+
* turn is silently created in memory but never persisted or broadcast,
|
|
1298
|
+
* and every later event sees `before !== null` (peekTurn already finds
|
|
1299
|
+
* it), so `turn:created` never fires at all.
|
|
1158
1300
|
*/
|
|
1159
1301
|
async processEventAndUpdateTurn(sessionId, event) {
|
|
1160
|
-
const
|
|
1302
|
+
const turnId = this.assembler.getTurnIdForEvent(event);
|
|
1303
|
+
if (!turnId) {
|
|
1304
|
+
return null;
|
|
1305
|
+
}
|
|
1306
|
+
const liveBefore = this.assembler.peekTurn(turnId);
|
|
1307
|
+
const before = liveBefore ? structuredClone(liveBefore) : null;
|
|
1308
|
+
const returnedTurn = await this.assembler.processEventAsync(event, async (sid) => {
|
|
1161
1309
|
return this.reserveNextTurnSequence(sid);
|
|
1162
1310
|
});
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
}
|
|
1167
|
-
await this.storeTurnSnapshot(sessionId, turn);
|
|
1168
|
-
return turn;
|
|
1311
|
+
const turn = returnedTurn ?? this.assembler.peekTurn(turnId);
|
|
1312
|
+
if (!turn) {
|
|
1313
|
+
return null;
|
|
1169
1314
|
}
|
|
1170
|
-
|
|
1315
|
+
if (event.runId && turn.type === "assistant" && !turn.metadata.runId) {
|
|
1316
|
+
turn.metadata.runId = event.runId;
|
|
1317
|
+
}
|
|
1318
|
+
const seq = event.sessionSeq ?? await this.getCurrentSessionSeq(sessionId);
|
|
1319
|
+
const deltas = diffTurn(before, turn, seq);
|
|
1320
|
+
if (deltas.length === 0) {
|
|
1321
|
+
return null;
|
|
1322
|
+
}
|
|
1323
|
+
await this.storeTurnSnapshot(sessionId, turn);
|
|
1324
|
+
return { turn, deltas };
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Current projection of a session: its full turn list plus the seq cursor
|
|
1328
|
+
* it reflects. Used for the cold-start snapshot AND as the gap-recovery
|
|
1329
|
+
* resume payload (see ADR note in the sync design doc) — both send the
|
|
1330
|
+
* same shape, just at different points in the connection lifecycle.
|
|
1331
|
+
*/
|
|
1332
|
+
async getProjection(sessionId) {
|
|
1333
|
+
const [turns, seq] = await Promise.all([
|
|
1334
|
+
this.getTurns(sessionId),
|
|
1335
|
+
this.getCurrentSessionSeq(sessionId)
|
|
1336
|
+
]);
|
|
1337
|
+
return { turns, seq };
|
|
1171
1338
|
}
|
|
1172
1339
|
/**
|
|
1173
1340
|
* Store extracted trace artifacts for a completed run.
|