@agentick/core 0.2.0 → 0.3.0
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/README.md +270 -64
- package/dist/.tsbuildinfo.build +1 -1
- package/dist/agentick-instance.d.ts.map +1 -1
- package/dist/agentick-instance.js +125 -119
- package/dist/agentick-instance.js.map +1 -1
- package/dist/app/session-store.d.ts +1 -1
- package/dist/app/session-store.js +1 -1
- package/dist/app/session.d.ts +26 -17
- package/dist/app/session.d.ts.map +1 -1
- package/dist/app/session.js +222 -204
- package/dist/app/session.js.map +1 -1
- package/dist/app/types.d.ts +230 -149
- package/dist/app/types.d.ts.map +1 -1
- package/dist/com/object-model.d.ts +7 -4
- package/dist/com/object-model.d.ts.map +1 -1
- package/dist/com/object-model.js +13 -4
- package/dist/com/object-model.js.map +1 -1
- package/dist/compiler/collector.d.ts +1 -1
- package/dist/compiler/collector.js +1 -1
- package/dist/compiler/fiber-compiler.d.ts +16 -30
- package/dist/compiler/fiber-compiler.d.ts.map +1 -1
- package/dist/compiler/fiber-compiler.js +32 -72
- package/dist/compiler/fiber-compiler.js.map +1 -1
- package/dist/compiler/index.d.ts +1 -1
- package/dist/compiler/index.js +1 -1
- package/dist/compiler/scheduler.d.ts +3 -3
- package/dist/compiler/scheduler.js +4 -4
- package/dist/compiler/scheduler.js.map +1 -1
- package/dist/component/component.d.ts +6 -6
- package/dist/component/component.d.ts.map +1 -1
- package/dist/hooks/com-state.d.ts +18 -4
- package/dist/hooks/com-state.d.ts.map +1 -1
- package/dist/hooks/com-state.js +44 -15
- package/dist/hooks/com-state.js.map +1 -1
- package/dist/hooks/context-info.d.ts +2 -35
- package/dist/hooks/context-info.d.ts.map +1 -1
- package/dist/hooks/context-info.js +8 -0
- package/dist/hooks/context-info.js.map +1 -1
- package/dist/hooks/context.d.ts +2 -3
- package/dist/hooks/context.d.ts.map +1 -1
- package/dist/hooks/context.js +2 -3
- package/dist/hooks/context.js.map +1 -1
- package/dist/hooks/data.d.ts +19 -2
- package/dist/hooks/data.d.ts.map +1 -1
- package/dist/hooks/data.js +14 -3
- package/dist/hooks/data.js.map +1 -1
- package/dist/hooks/formatter-context.d.ts +1 -2
- package/dist/hooks/formatter-context.d.ts.map +1 -1
- package/dist/hooks/formatter-context.js +1 -2
- package/dist/hooks/formatter-context.js.map +1 -1
- package/dist/hooks/index.d.ts +6 -4
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +6 -2
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/message-context.d.ts +1 -1
- package/dist/hooks/message-context.js +1 -1
- package/dist/hooks/resolved.d.ts +2 -0
- package/dist/hooks/resolved.d.ts.map +1 -0
- package/dist/hooks/resolved.js +6 -0
- package/dist/hooks/resolved.js.map +1 -0
- package/dist/hooks/runtime-context.d.ts +46 -1
- package/dist/hooks/runtime-context.d.ts.map +1 -1
- package/dist/hooks/runtime-context.js +36 -1
- package/dist/hooks/runtime-context.js.map +1 -1
- package/dist/hooks/timeline.d.ts +10 -0
- package/dist/hooks/timeline.d.ts.map +1 -0
- package/dist/hooks/timeline.js +13 -0
- package/dist/hooks/timeline.js.map +1 -0
- package/dist/hooks/types.d.ts +1 -1
- package/dist/hooks/types.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/jsx/components/timeline.d.ts.map +1 -1
- package/dist/jsx/components/timeline.js +11 -11
- package/dist/jsx/components/timeline.js.map +1 -1
- package/dist/jsx/jsx-runtime.d.ts +1 -3
- package/dist/jsx/jsx-runtime.d.ts.map +1 -1
- package/dist/local-transport.d.ts +31 -0
- package/dist/local-transport.d.ts.map +1 -0
- package/dist/local-transport.js +119 -0
- package/dist/local-transport.js.map +1 -0
- package/dist/model/model.d.ts +0 -2
- package/dist/model/model.d.ts.map +1 -1
- package/dist/model/model.js.map +1 -1
- package/dist/procedure/index.d.ts.map +1 -1
- package/dist/reconciler/host-config.d.ts +6 -5
- package/dist/reconciler/host-config.d.ts.map +1 -1
- package/dist/reconciler/host-config.js +56 -27
- package/dist/reconciler/host-config.js.map +1 -1
- package/dist/reconciler/index.d.ts +1 -1
- package/dist/reconciler/index.js +1 -1
- package/dist/reconciler/reconciler.d.ts +12 -11
- package/dist/reconciler/reconciler.d.ts.map +1 -1
- package/dist/reconciler/reconciler.js +23 -22
- package/dist/reconciler/reconciler.js.map +1 -1
- package/dist/reconciler/types.d.ts +2 -8
- package/dist/reconciler/types.d.ts.map +1 -1
- package/dist/reconciler/types.js +2 -2
- package/dist/reconciler/types.js.map +1 -1
- package/dist/renderers/types.d.ts +1 -1
- package/dist/renderers/types.js +1 -1
- package/dist/testing/act.d.ts.map +1 -1
- package/dist/testing/act.js +2 -3
- package/dist/testing/act.js.map +1 -1
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +2 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/mock-app.d.ts.map +1 -1
- package/dist/testing/mock-app.js +5 -15
- package/dist/testing/mock-app.js.map +1 -1
- package/dist/testing/mocks.d.ts +2 -3
- package/dist/testing/mocks.d.ts.map +1 -1
- package/dist/testing/mocks.js +2 -3
- package/dist/testing/mocks.js.map +1 -1
- package/dist/testing/render-agent.d.ts +1 -1
- package/dist/testing/render-agent.d.ts.map +1 -1
- package/dist/testing/render-agent.js +5 -5
- package/dist/testing/render-agent.js.map +1 -1
- package/dist/testing/test-environment.d.ts +122 -0
- package/dist/testing/test-environment.d.ts.map +1 -0
- package/dist/testing/test-environment.js +126 -0
- package/dist/testing/test-environment.js.map +1 -0
- package/package.json +15 -15
- package/dist/hibernation/index.d.ts +0 -126
- package/dist/hibernation/index.d.ts.map +0 -1
- package/dist/hibernation/index.js +0 -127
- package/dist/hibernation/index.js.map +0 -1
package/dist/app/session.js
CHANGED
|
@@ -58,18 +58,17 @@ export class SessionImpl extends EventEmitter {
|
|
|
58
58
|
_tick = 1;
|
|
59
59
|
_isAborted = false;
|
|
60
60
|
_currentExecutionId = null;
|
|
61
|
-
// Hydration state (pending fiber tree data to restore)
|
|
62
|
-
_pendingHydrationData = null;
|
|
63
61
|
// Compilation infrastructure (no intermediate layer)
|
|
64
62
|
compiler = null;
|
|
65
63
|
ctx = null;
|
|
66
64
|
structureRenderer = null;
|
|
67
65
|
scheduler = null;
|
|
68
|
-
//
|
|
69
|
-
|
|
66
|
+
// Last completed tick's compiled output. Used only by inspect().lastOutput.
|
|
67
|
+
_lastCompleteOutput = null;
|
|
70
68
|
_currentOutput = null;
|
|
71
|
-
//
|
|
72
|
-
|
|
69
|
+
// Session-owned timeline (source of truth, append-only)
|
|
70
|
+
_timeline = [];
|
|
71
|
+
_maxTimelineEntries;
|
|
73
72
|
// Estimated context tokens from last compilation (pre-model-call)
|
|
74
73
|
_estimatedContextTokens;
|
|
75
74
|
// Track last published timeline length for delta publishing
|
|
@@ -113,8 +112,12 @@ export class SessionImpl extends EventEmitter {
|
|
|
113
112
|
_currentHandle = null;
|
|
114
113
|
_currentResultResolve = null;
|
|
115
114
|
_currentResultReject = null;
|
|
116
|
-
//
|
|
117
|
-
|
|
115
|
+
// Auto-persist callback (set by App when store is configured)
|
|
116
|
+
_persistCallback = null;
|
|
117
|
+
// Snapshot for resolve (set when restoring from store)
|
|
118
|
+
_snapshotForResolve = null;
|
|
119
|
+
// Execution environment initialization tracking
|
|
120
|
+
_environmentInitialized = false;
|
|
118
121
|
// Spawn hierarchy
|
|
119
122
|
_parent = null;
|
|
120
123
|
_children = [];
|
|
@@ -153,21 +156,10 @@ export class SessionImpl extends EventEmitter {
|
|
|
153
156
|
});
|
|
154
157
|
}
|
|
155
158
|
}
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
// Seed initial timeline if provided
|
|
161
|
-
if (sessionOptions.initialTimeline?.length) {
|
|
162
|
-
this._previousOutput = {
|
|
163
|
-
timeline: sessionOptions.initialTimeline,
|
|
164
|
-
system: [],
|
|
165
|
-
ephemeral: [],
|
|
166
|
-
sections: {},
|
|
167
|
-
tools: [],
|
|
168
|
-
metadata: {},
|
|
169
|
-
};
|
|
170
|
-
}
|
|
159
|
+
// Read maxTimelineEntries from appOptions
|
|
160
|
+
this._maxTimelineEntries = appOptions.maxTimelineEntries;
|
|
161
|
+
// Note: snapshot/initialTimeline hydration now handled via _snapshotForResolve
|
|
162
|
+
// set by App.createSessionFromSnapshot() — applied in ensureCompilationInfrastructure()
|
|
171
163
|
// Start recording if enabled via options
|
|
172
164
|
if (sessionOptions.recording) {
|
|
173
165
|
this.startRecording(sessionOptions.recording);
|
|
@@ -181,6 +173,14 @@ export class SessionImpl extends EventEmitter {
|
|
|
181
173
|
get status() {
|
|
182
174
|
return this._status;
|
|
183
175
|
}
|
|
176
|
+
/** Whether the session is in a terminal state (closed). */
|
|
177
|
+
get isTerminal() {
|
|
178
|
+
return this._status === "closed";
|
|
179
|
+
}
|
|
180
|
+
/** Error message for operations attempted on a terminal session. */
|
|
181
|
+
get terminalError() {
|
|
182
|
+
return "Session is closed";
|
|
183
|
+
}
|
|
184
184
|
get currentTick() {
|
|
185
185
|
return this._tick;
|
|
186
186
|
}
|
|
@@ -233,8 +233,8 @@ export class SessionImpl extends EventEmitter {
|
|
|
233
233
|
handleFactory: false,
|
|
234
234
|
executionBoundary: false,
|
|
235
235
|
}, async (message) => {
|
|
236
|
-
if (this.
|
|
237
|
-
throw new Error(
|
|
236
|
+
if (this.isTerminal) {
|
|
237
|
+
throw new Error(this.terminalError);
|
|
238
238
|
}
|
|
239
239
|
const messageWithId = ensureMessageId(message);
|
|
240
240
|
this._queuedMessages.push(messageWithId);
|
|
@@ -254,6 +254,7 @@ export class SessionImpl extends EventEmitter {
|
|
|
254
254
|
};
|
|
255
255
|
const tickState = {
|
|
256
256
|
tick: this._tick,
|
|
257
|
+
timeline: this._timeline,
|
|
257
258
|
stop: () => { },
|
|
258
259
|
queuedMessages: [],
|
|
259
260
|
};
|
|
@@ -267,8 +268,8 @@ export class SessionImpl extends EventEmitter {
|
|
|
267
268
|
handleFactory: false,
|
|
268
269
|
executionBoundary: false,
|
|
269
270
|
}, async (input) => {
|
|
270
|
-
if (this.
|
|
271
|
-
throw new Error(
|
|
271
|
+
if (this.isTerminal) {
|
|
272
|
+
throw new Error(this.terminalError);
|
|
272
273
|
}
|
|
273
274
|
const { messages = [], props, metadata, maxTicks, signal } = input;
|
|
274
275
|
// Apply metadata to messages
|
|
@@ -296,6 +297,7 @@ export class SessionImpl extends EventEmitter {
|
|
|
296
297
|
};
|
|
297
298
|
const tickState = {
|
|
298
299
|
tick: this._tick,
|
|
300
|
+
timeline: this._timeline,
|
|
299
301
|
stop: () => { },
|
|
300
302
|
queuedMessages: [],
|
|
301
303
|
};
|
|
@@ -330,8 +332,8 @@ export class SessionImpl extends EventEmitter {
|
|
|
330
332
|
handleFactory: false,
|
|
331
333
|
executionBoundary: false,
|
|
332
334
|
}, (props, options) => {
|
|
333
|
-
if (this.
|
|
334
|
-
throw new Error(
|
|
335
|
+
if (this.isTerminal) {
|
|
336
|
+
throw new Error(this.terminalError);
|
|
335
337
|
}
|
|
336
338
|
// Props is explicitly provided (even if empty object) - always run tick
|
|
337
339
|
// Only skip if props is undefined/null AND no queued messages
|
|
@@ -357,25 +359,32 @@ export class SessionImpl extends EventEmitter {
|
|
|
357
359
|
metadata: { operation: "spawn" },
|
|
358
360
|
handleFactory: false,
|
|
359
361
|
executionBoundary: false,
|
|
360
|
-
}, async (component, input
|
|
361
|
-
if (this.
|
|
362
|
-
throw new Error(
|
|
362
|
+
}, async (component, input, spawnOptions) => {
|
|
363
|
+
if (this.isTerminal) {
|
|
364
|
+
throw new Error(this.terminalError);
|
|
363
365
|
}
|
|
364
366
|
if (this._spawnDepth >= SessionImpl.MAX_SPAWN_DEPTH) {
|
|
365
367
|
throw new Error(`Maximum spawn depth (${SessionImpl.MAX_SPAWN_DEPTH}) exceeded`);
|
|
366
368
|
}
|
|
367
369
|
// 1. Resolve to ComponentFunction
|
|
368
|
-
const
|
|
370
|
+
const resolvedInput = input ?? {};
|
|
371
|
+
const { Component, mergedProps } = this.resolveSpawnTarget(component, resolvedInput);
|
|
369
372
|
// 2. Create child SessionImpl (ephemeral — NOT registered in App's registry)
|
|
370
373
|
// Whitelist structural fields only — lifecycle callbacks, session management,
|
|
371
374
|
// signal, and devTools are intentionally excluded. New AppOptions fields
|
|
372
375
|
// must be explicitly added here if children should inherit them.
|
|
376
|
+
//
|
|
377
|
+
// NOTE: `environment` IS inherited by default. A REPL environment, sandbox,
|
|
378
|
+
// or human-in-the-loop gateway should apply to sub-agents — the execution
|
|
379
|
+
// model is structural, not observational (unlike lifecycle callbacks).
|
|
380
|
+
// Use SpawnOptions to override for specific children.
|
|
373
381
|
const childAppOptions = {
|
|
374
|
-
model: this.appOptions.model,
|
|
382
|
+
model: spawnOptions?.model ?? this.appOptions.model,
|
|
375
383
|
tools: this.appOptions.tools,
|
|
376
384
|
mcpServers: this.appOptions.mcpServers,
|
|
377
|
-
maxTicks: this.appOptions.maxTicks,
|
|
385
|
+
maxTicks: spawnOptions?.maxTicks ?? this.appOptions.maxTicks,
|
|
378
386
|
inheritDefaults: this.appOptions.inheritDefaults,
|
|
387
|
+
environment: spawnOptions?.environment ?? this.appOptions.environment,
|
|
379
388
|
};
|
|
380
389
|
const childOptions = {
|
|
381
390
|
signal: this.executionAbortController?.signal,
|
|
@@ -387,14 +396,14 @@ export class SessionImpl extends EventEmitter {
|
|
|
387
396
|
this._children.push(child);
|
|
388
397
|
// 3. Delegate to child.send()
|
|
389
398
|
const handle = await child.send({
|
|
390
|
-
...
|
|
399
|
+
...resolvedInput,
|
|
391
400
|
props: mergedProps,
|
|
392
401
|
});
|
|
393
402
|
// 4. Cleanup on completion
|
|
394
403
|
handle.result
|
|
395
|
-
.finally(() => {
|
|
404
|
+
.finally(async () => {
|
|
396
405
|
this._children = this._children.filter((c) => c !== child);
|
|
397
|
-
child.close();
|
|
406
|
+
await child.close();
|
|
398
407
|
})
|
|
399
408
|
.catch(() => { });
|
|
400
409
|
return handle;
|
|
@@ -648,8 +657,8 @@ export class SessionImpl extends EventEmitter {
|
|
|
648
657
|
// Interrupt & Abort
|
|
649
658
|
// ════════════════════════════════════════════════════════════════════════
|
|
650
659
|
interrupt(message, reason) {
|
|
651
|
-
if (this.
|
|
652
|
-
throw new Error(
|
|
660
|
+
if (this.isTerminal) {
|
|
661
|
+
throw new Error(this.terminalError);
|
|
653
662
|
}
|
|
654
663
|
if (message) {
|
|
655
664
|
this._queuedMessages.push(message);
|
|
@@ -788,37 +797,29 @@ export class SessionImpl extends EventEmitter {
|
|
|
788
797
|
version: "1.0",
|
|
789
798
|
sessionId: this.id,
|
|
790
799
|
tick: this._tick,
|
|
791
|
-
timeline: this.
|
|
792
|
-
|
|
800
|
+
timeline: [...this._timeline],
|
|
801
|
+
comState: this.compiler
|
|
802
|
+
? this.compiler.getSerializableComState(this.ctx?.getStateAll() ?? {})
|
|
803
|
+
: {},
|
|
804
|
+
dataCache: this.compiler?.getSerializableDataCache() ?? {},
|
|
793
805
|
usage: { ...this._totalUsage },
|
|
794
806
|
timestamp: Date.now(),
|
|
795
807
|
};
|
|
796
808
|
}
|
|
797
809
|
/**
|
|
798
|
-
* Set the
|
|
799
|
-
* Called by the App when
|
|
810
|
+
* Set the auto-persist callback.
|
|
811
|
+
* Called by the App when a store is configured.
|
|
800
812
|
* @internal
|
|
801
813
|
*/
|
|
802
|
-
|
|
803
|
-
this.
|
|
814
|
+
setPersistCallback(callback) {
|
|
815
|
+
this._persistCallback = callback;
|
|
804
816
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
this.interrupt(undefined, "Hibernation requested");
|
|
812
|
-
}
|
|
813
|
-
// Delegate to the App's hibernate callback if available
|
|
814
|
-
if (this._hibernateCallback) {
|
|
815
|
-
return this._hibernateCallback();
|
|
816
|
-
}
|
|
817
|
-
// No callback - just return the snapshot without persisting
|
|
818
|
-
// This allows sessions created outside of App management to still hibernate
|
|
819
|
-
const snapshot = this.snapshot();
|
|
820
|
-
this.close();
|
|
821
|
-
return snapshot;
|
|
817
|
+
/**
|
|
818
|
+
* Set a snapshot to be applied/resolved when compilation infrastructure is created.
|
|
819
|
+
* @internal
|
|
820
|
+
*/
|
|
821
|
+
setSnapshotForResolve(snapshot) {
|
|
822
|
+
this._snapshotForResolve = snapshot;
|
|
822
823
|
}
|
|
823
824
|
inspect() {
|
|
824
825
|
// Get fiber summary for component/hook stats
|
|
@@ -834,7 +835,7 @@ export class SessionImpl extends EventEmitter {
|
|
|
834
835
|
queuedMessages: [...this._queuedMessages],
|
|
835
836
|
currentPhase: this._status === "running" ? "model" : undefined, // Approximate
|
|
836
837
|
isAborted: this._isAborted,
|
|
837
|
-
lastOutput: this.
|
|
838
|
+
lastOutput: this._lastCompleteOutput,
|
|
838
839
|
lastModelOutput: this._lastModelOutput,
|
|
839
840
|
lastToolCalls: lastSnapshot?.tools.calls ?? [],
|
|
840
841
|
lastToolResults: lastSnapshot?.tools.results.map((r) => ({
|
|
@@ -1200,16 +1201,23 @@ export class SessionImpl extends EventEmitter {
|
|
|
1200
1201
|
}
|
|
1201
1202
|
}
|
|
1202
1203
|
// ════════════════════════════════════════════════════════════════════════
|
|
1203
|
-
// Close
|
|
1204
|
+
// Close & Teardown
|
|
1204
1205
|
// ════════════════════════════════════════════════════════════════════════
|
|
1205
|
-
close() {
|
|
1206
|
-
if (this.
|
|
1206
|
+
async close() {
|
|
1207
|
+
if (this.isTerminal)
|
|
1207
1208
|
return;
|
|
1208
1209
|
this._status = "closed";
|
|
1209
|
-
//
|
|
1210
|
-
|
|
1211
|
-
|
|
1210
|
+
// Notify execution environment of destroy
|
|
1211
|
+
if (this._environmentInitialized && this.appOptions.environment?.onDestroy) {
|
|
1212
|
+
try {
|
|
1213
|
+
await this.appOptions.environment.onDestroy(this);
|
|
1214
|
+
}
|
|
1215
|
+
catch (err) {
|
|
1216
|
+
this.log.warn({ error: err }, "Environment onDestroy failed");
|
|
1217
|
+
}
|
|
1212
1218
|
}
|
|
1219
|
+
// Close all child sessions
|
|
1220
|
+
await Promise.all(this._children.map((child) => child.close()));
|
|
1213
1221
|
this._children = [];
|
|
1214
1222
|
this.sessionAbortController.abort("Session closed");
|
|
1215
1223
|
this.executionAbortController?.abort("Session closed");
|
|
@@ -1226,7 +1234,12 @@ export class SessionImpl extends EventEmitter {
|
|
|
1226
1234
|
}
|
|
1227
1235
|
// Unmount compiler if it exists
|
|
1228
1236
|
if (this.compiler) {
|
|
1229
|
-
|
|
1237
|
+
try {
|
|
1238
|
+
await this.compiler.unmount();
|
|
1239
|
+
}
|
|
1240
|
+
catch {
|
|
1241
|
+
// Unmount errors during close are non-fatal
|
|
1242
|
+
}
|
|
1230
1243
|
this.compiler = null;
|
|
1231
1244
|
}
|
|
1232
1245
|
this.ctx = null;
|
|
@@ -1268,29 +1281,6 @@ export class SessionImpl extends EventEmitter {
|
|
|
1268
1281
|
const merged = { ...baseContext, ...(current ?? {}), ...sessionContext };
|
|
1269
1282
|
return Context.run(merged, fn);
|
|
1270
1283
|
}
|
|
1271
|
-
hydrate(snapshot) {
|
|
1272
|
-
this._tick = snapshot.tick;
|
|
1273
|
-
// Hydrate timeline (conversation history)
|
|
1274
|
-
if (snapshot.timeline) {
|
|
1275
|
-
this._previousOutput = {
|
|
1276
|
-
timeline: snapshot.timeline,
|
|
1277
|
-
system: [],
|
|
1278
|
-
ephemeral: [],
|
|
1279
|
-
sections: {},
|
|
1280
|
-
tools: [],
|
|
1281
|
-
metadata: {},
|
|
1282
|
-
};
|
|
1283
|
-
}
|
|
1284
|
-
// Hydrate usage stats
|
|
1285
|
-
if (snapshot.usage) {
|
|
1286
|
-
this._totalUsage = { ...snapshot.usage };
|
|
1287
|
-
}
|
|
1288
|
-
// Hydrate fiber tree (component state)
|
|
1289
|
-
// This will be applied when the compiler is first created
|
|
1290
|
-
if (snapshot.componentState) {
|
|
1291
|
-
this._pendingHydrationData = snapshot.componentState;
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
1284
|
/**
|
|
1295
1285
|
* The core tick execution loop.
|
|
1296
1286
|
*
|
|
@@ -1300,8 +1290,8 @@ export class SessionImpl extends EventEmitter {
|
|
|
1300
1290
|
* 3. Return result
|
|
1301
1291
|
*/
|
|
1302
1292
|
async executeTick(props, options) {
|
|
1303
|
-
if (this.
|
|
1304
|
-
throw new Error(
|
|
1293
|
+
if (this.isTerminal) {
|
|
1294
|
+
throw new Error(this.terminalError);
|
|
1305
1295
|
}
|
|
1306
1296
|
const signal = this.executionAbortController?.signal ?? this.sessionAbortController.signal;
|
|
1307
1297
|
if (signal.aborted) {
|
|
@@ -1345,7 +1335,7 @@ export class SessionImpl extends EventEmitter {
|
|
|
1345
1335
|
let stopReason;
|
|
1346
1336
|
let output;
|
|
1347
1337
|
const outputs = {};
|
|
1348
|
-
|
|
1338
|
+
let responseText = "";
|
|
1349
1339
|
const toolExecutor = new ToolExecutor();
|
|
1350
1340
|
this.emitEvent({
|
|
1351
1341
|
type: "execution_start",
|
|
@@ -1429,7 +1419,11 @@ export class SessionImpl extends EventEmitter {
|
|
|
1429
1419
|
if (signal.aborted) {
|
|
1430
1420
|
throw new AbortError("Execution aborted", signal.reason);
|
|
1431
1421
|
}
|
|
1432
|
-
|
|
1422
|
+
let modelInput = compiled.modelInput ?? compiled.formatted;
|
|
1423
|
+
// Apply execution environment transformation
|
|
1424
|
+
if (this.appOptions.environment?.prepareModelInput) {
|
|
1425
|
+
modelInput = await this.appOptions.environment.prepareModelInput(modelInput, (compiled.tools ?? []));
|
|
1426
|
+
}
|
|
1433
1427
|
const modelStartTime = Date.now();
|
|
1434
1428
|
// Emit model request to DevTools
|
|
1435
1429
|
// modelInput is the Agentick ModelInput format (after fromEngineState transformation)
|
|
@@ -1458,17 +1452,12 @@ export class SessionImpl extends EventEmitter {
|
|
|
1458
1452
|
// Stream model output if supported
|
|
1459
1453
|
let modelOutput;
|
|
1460
1454
|
if (model.stream) {
|
|
1461
|
-
const streamEvents = [];
|
|
1462
1455
|
const streamIterable = await model.stream(modelInput);
|
|
1463
1456
|
for await (const event of streamIterable) {
|
|
1464
1457
|
if (signal.aborted) {
|
|
1465
1458
|
throw new AbortError("Execution aborted", signal.reason);
|
|
1466
1459
|
}
|
|
1467
|
-
streamEvents.push(event);
|
|
1468
1460
|
this.emitEvent(event);
|
|
1469
|
-
if (event.type === "content_delta" && "delta" in event) {
|
|
1470
|
-
responseChunks.push(event.delta);
|
|
1471
|
-
}
|
|
1472
1461
|
if (event.type === "message" && "message" in event) {
|
|
1473
1462
|
const messageEvent = event;
|
|
1474
1463
|
modelOutput = {
|
|
@@ -1480,27 +1469,8 @@ export class SessionImpl extends EventEmitter {
|
|
|
1480
1469
|
};
|
|
1481
1470
|
}
|
|
1482
1471
|
}
|
|
1483
|
-
// Aggregate stream events into final output
|
|
1484
1472
|
if (!modelOutput) {
|
|
1485
|
-
|
|
1486
|
-
// Use model's processStream if available
|
|
1487
|
-
modelOutput = await model.processStream(streamEvents);
|
|
1488
|
-
}
|
|
1489
|
-
else if (responseChunks.length > 0) {
|
|
1490
|
-
// Construct from collected chunks
|
|
1491
|
-
const text = responseChunks.join("");
|
|
1492
|
-
modelOutput = {
|
|
1493
|
-
message: {
|
|
1494
|
-
role: "assistant",
|
|
1495
|
-
content: [{ type: "text", text }],
|
|
1496
|
-
},
|
|
1497
|
-
stopReason: "stop",
|
|
1498
|
-
raw: { streamed: true },
|
|
1499
|
-
};
|
|
1500
|
-
}
|
|
1501
|
-
else {
|
|
1502
|
-
throw new Error("Streaming completed but no response was received");
|
|
1503
|
-
}
|
|
1473
|
+
throw new Error("Streaming completed but no model output was received");
|
|
1504
1474
|
}
|
|
1505
1475
|
}
|
|
1506
1476
|
else {
|
|
@@ -1516,8 +1486,7 @@ export class SessionImpl extends EventEmitter {
|
|
|
1516
1486
|
if (modelOutput?.message) {
|
|
1517
1487
|
const textContent = modelOutput.message.content?.find((b) => b.type === "text");
|
|
1518
1488
|
if (textContent && "text" in textContent) {
|
|
1519
|
-
|
|
1520
|
-
responseChunks.push(textContent.text);
|
|
1489
|
+
responseText = textContent.text;
|
|
1521
1490
|
}
|
|
1522
1491
|
}
|
|
1523
1492
|
// Update usage
|
|
@@ -1625,9 +1594,9 @@ export class SessionImpl extends EventEmitter {
|
|
|
1625
1594
|
// These can call tickResult.stop() or tickResult.continue() to influence continuation
|
|
1626
1595
|
const tickEndState = {
|
|
1627
1596
|
tick: currentTick,
|
|
1628
|
-
previous: this._previousOutput ?? undefined,
|
|
1629
1597
|
current: this._currentOutput,
|
|
1630
1598
|
queuedMessages: [],
|
|
1599
|
+
timeline: this._timeline,
|
|
1631
1600
|
stop: () => { }, // No-op at tick end - use tickResult.stop() instead
|
|
1632
1601
|
};
|
|
1633
1602
|
await this.compiler?.notifyTickEnd(tickEndState, tickResult);
|
|
@@ -1666,24 +1635,18 @@ export class SessionImpl extends EventEmitter {
|
|
|
1666
1635
|
shouldContinue,
|
|
1667
1636
|
stopReason,
|
|
1668
1637
|
});
|
|
1669
|
-
// Update _previousOutput after each tick so the next tick has access to the timeline
|
|
1670
|
-
// This is critical for <Timeline> to render conversation history on subsequent ticks
|
|
1671
1638
|
output = await this.complete();
|
|
1672
|
-
this.
|
|
1673
|
-
this.log.debug({ timelineLength: output.timeline?.length ?? 0 }, "Updated _previousOutput after tick");
|
|
1639
|
+
this._lastCompleteOutput = output;
|
|
1674
1640
|
this._tick++;
|
|
1675
1641
|
usage.ticks = (usage.ticks ?? 0) + 1;
|
|
1676
1642
|
}
|
|
1677
|
-
// Final complete (may be redundant but ensures consistency)
|
|
1678
|
-
output = await this.complete();
|
|
1679
|
-
this._previousOutput = output;
|
|
1680
1643
|
// Accumulate usage
|
|
1681
1644
|
this._totalUsage.inputTokens += usage.inputTokens;
|
|
1682
1645
|
this._totalUsage.outputTokens += usage.outputTokens;
|
|
1683
1646
|
this._totalUsage.totalTokens += usage.totalTokens;
|
|
1684
1647
|
this._totalUsage.ticks = (this._totalUsage.ticks ?? 0) + (usage.ticks ?? 0);
|
|
1685
1648
|
const resultPayload = {
|
|
1686
|
-
response:
|
|
1649
|
+
response: responseText,
|
|
1687
1650
|
outputs,
|
|
1688
1651
|
usage,
|
|
1689
1652
|
stopReason,
|
|
@@ -1708,6 +1671,16 @@ export class SessionImpl extends EventEmitter {
|
|
|
1708
1671
|
output: output ?? null,
|
|
1709
1672
|
timestamp: timestamp(),
|
|
1710
1673
|
});
|
|
1674
|
+
// Auto-persist snapshot after successful execution (fire-and-forget, skip on abort)
|
|
1675
|
+
if (this._persistCallback && !this._isAborted) {
|
|
1676
|
+
let snap = this.snapshot();
|
|
1677
|
+
if (this.appOptions.environment?.onPersist) {
|
|
1678
|
+
snap = await this.appOptions.environment.onPersist(this, snap);
|
|
1679
|
+
}
|
|
1680
|
+
this._persistCallback(snap).catch((err) => {
|
|
1681
|
+
this.log.warn({ error: err }, "Auto-persist failed");
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1711
1684
|
// Publish timeline delta to channel for real-time sync across clients
|
|
1712
1685
|
// Only send NEW messages since last publish - O(delta) not O(n)
|
|
1713
1686
|
if (output?.timeline) {
|
|
@@ -1738,7 +1711,7 @@ export class SessionImpl extends EventEmitter {
|
|
|
1738
1711
|
this._status = "idle";
|
|
1739
1712
|
}
|
|
1740
1713
|
return {
|
|
1741
|
-
response:
|
|
1714
|
+
response: responseText,
|
|
1742
1715
|
outputs,
|
|
1743
1716
|
usage,
|
|
1744
1717
|
stopReason,
|
|
@@ -1759,12 +1732,13 @@ export class SessionImpl extends EventEmitter {
|
|
|
1759
1732
|
this.ctx = new COM({
|
|
1760
1733
|
metadata: {},
|
|
1761
1734
|
});
|
|
1762
|
-
this.compiler = new FiberCompiler(this.ctx
|
|
1763
|
-
//
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1735
|
+
this.compiler = new FiberCompiler(this.ctx);
|
|
1736
|
+
// Wire timeline accessors to RuntimeStore
|
|
1737
|
+
const runtimeStore = this.compiler.getRuntimeStore();
|
|
1738
|
+
runtimeStore.getSessionTimeline = () => this._timeline;
|
|
1739
|
+
runtimeStore.setSessionTimeline = (entries) => {
|
|
1740
|
+
this._timeline = [...entries];
|
|
1741
|
+
};
|
|
1768
1742
|
// Create scheduler and wire it to the compiler
|
|
1769
1743
|
// This enables the reactive model: state changes between ticks trigger reconciliation
|
|
1770
1744
|
this.scheduler = new ReconciliationScheduler(this.compiler, {
|
|
@@ -1793,6 +1767,75 @@ export class SessionImpl extends EventEmitter {
|
|
|
1793
1767
|
}
|
|
1794
1768
|
// Notify compiler that compilation is starting
|
|
1795
1769
|
await this.compiler.notifyStart();
|
|
1770
|
+
// Apply snapshot-for-resolve if set (restore from store)
|
|
1771
|
+
if (this._snapshotForResolve) {
|
|
1772
|
+
try {
|
|
1773
|
+
const resolveConfig = this.appOptions.resolve;
|
|
1774
|
+
if (resolveConfig) {
|
|
1775
|
+
// Layer 2: resolve controls reconstruction
|
|
1776
|
+
const ctx = { sessionId: this.id, snapshot: this._snapshotForResolve };
|
|
1777
|
+
const resolved = await this.executeResolve(resolveConfig, ctx);
|
|
1778
|
+
runtimeStore.resolvedData = resolved;
|
|
1779
|
+
}
|
|
1780
|
+
else {
|
|
1781
|
+
// Layer 1: auto-apply snapshot
|
|
1782
|
+
const snap = this._snapshotForResolve;
|
|
1783
|
+
this._timeline = [...(snap.timeline ?? [])];
|
|
1784
|
+
this._tick = snap.tick;
|
|
1785
|
+
if (snap.usage)
|
|
1786
|
+
this._totalUsage = { ...snap.usage };
|
|
1787
|
+
if (snap.comState && Object.keys(snap.comState).length > 0) {
|
|
1788
|
+
this.ctx.setStatePartial(snap.comState);
|
|
1789
|
+
}
|
|
1790
|
+
if (snap.dataCache && Object.keys(snap.dataCache).length > 0) {
|
|
1791
|
+
this.compiler.setDataCache(snap.dataCache);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
// Notify execution environment of restore
|
|
1795
|
+
if (this.appOptions.environment?.onRestore) {
|
|
1796
|
+
await this.appOptions.environment.onRestore(this, this._snapshotForResolve);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
finally {
|
|
1800
|
+
this._snapshotForResolve = null;
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
// Initialize execution environment (once per session lifecycle)
|
|
1804
|
+
if (!this._environmentInitialized) {
|
|
1805
|
+
if (this.appOptions.environment?.onSessionInit) {
|
|
1806
|
+
await this.appOptions.environment.onSessionInit(this);
|
|
1807
|
+
}
|
|
1808
|
+
this._environmentInitialized = true;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
/**
|
|
1812
|
+
* Execute resolve configuration and return results.
|
|
1813
|
+
*/
|
|
1814
|
+
async executeResolve(config, ctx) {
|
|
1815
|
+
if (typeof config === "function") {
|
|
1816
|
+
try {
|
|
1817
|
+
return await config(ctx);
|
|
1818
|
+
}
|
|
1819
|
+
catch (err) {
|
|
1820
|
+
throw new Error(`resolve function failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
// Object form: resolve each entry
|
|
1824
|
+
const results = {};
|
|
1825
|
+
for (const [key, value] of Object.entries(config)) {
|
|
1826
|
+
if (typeof value === "function") {
|
|
1827
|
+
try {
|
|
1828
|
+
results[key] = await value(ctx);
|
|
1829
|
+
}
|
|
1830
|
+
catch (err) {
|
|
1831
|
+
throw new Error(`resolve["${key}"] failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
else {
|
|
1835
|
+
results[key] = value;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
return results;
|
|
1796
1839
|
}
|
|
1797
1840
|
/**
|
|
1798
1841
|
* Compile a single tick.
|
|
@@ -1820,9 +1863,9 @@ export class SessionImpl extends EventEmitter {
|
|
|
1820
1863
|
// Prepare tick state
|
|
1821
1864
|
const tickState = {
|
|
1822
1865
|
tick: this._tick,
|
|
1823
|
-
previous: this._previousOutput ?? undefined,
|
|
1824
1866
|
current: this._currentOutput,
|
|
1825
1867
|
queuedMessages: queuedMessages,
|
|
1868
|
+
timeline: [...this._timeline],
|
|
1826
1869
|
stop: (reason) => {
|
|
1827
1870
|
tickState.stopReason = reason;
|
|
1828
1871
|
},
|
|
@@ -1847,21 +1890,16 @@ export class SessionImpl extends EventEmitter {
|
|
|
1847
1890
|
// Enter tick mode - scheduler will defer reconciliations until exitTick
|
|
1848
1891
|
this.scheduler?.enterTick();
|
|
1849
1892
|
let compiled;
|
|
1850
|
-
const wasHydrating = this.compiler.isHydratingNow();
|
|
1851
1893
|
try {
|
|
1852
1894
|
// Compile until stable
|
|
1853
1895
|
// Note: tickControl and getChannel are available for future use
|
|
1854
|
-
// but not currently used by
|
|
1896
|
+
// but not currently used by FiberCompiler
|
|
1855
1897
|
void tickControl;
|
|
1856
1898
|
void getChannel;
|
|
1857
1899
|
const result = await this.compiler.compileUntilStable(rootElement, tickState, {
|
|
1858
1900
|
maxIterations: 50,
|
|
1859
1901
|
});
|
|
1860
1902
|
compiled = result.compiled;
|
|
1861
|
-
// Complete hydration after first successful compile
|
|
1862
|
-
if (wasHydrating) {
|
|
1863
|
-
this.compiler.completeHydration();
|
|
1864
|
-
}
|
|
1865
1903
|
}
|
|
1866
1904
|
finally {
|
|
1867
1905
|
// Exit tick mode - any pending reconciliations will now flush
|
|
@@ -1872,8 +1910,6 @@ export class SessionImpl extends EventEmitter {
|
|
|
1872
1910
|
// Format input - compiled structure IS the complete projection
|
|
1873
1911
|
// JSX components render history as <Message>, so compiled.timelineEntries is complete
|
|
1874
1912
|
const formatted = await this.structureRenderer.formatInput(this.ctx.toInput());
|
|
1875
|
-
// Track what we're sending to the model (for combining with response in complete())
|
|
1876
|
-
this._lastSentTimeline = formatted.timeline ?? [];
|
|
1877
1913
|
// Track estimated context tokens for contextInfo
|
|
1878
1914
|
this._estimatedContextTokens = formatted.totalTokens;
|
|
1879
1915
|
// Get model from COM if not in options
|
|
@@ -1922,17 +1958,28 @@ export class SessionImpl extends EventEmitter {
|
|
|
1922
1958
|
});
|
|
1923
1959
|
continue;
|
|
1924
1960
|
}
|
|
1925
|
-
// Execute tool
|
|
1961
|
+
// Execute tool (optionally wrapped by execution environment)
|
|
1926
1962
|
try {
|
|
1927
|
-
const
|
|
1928
|
-
|
|
1963
|
+
const env = this.appOptions.environment;
|
|
1964
|
+
let toolResult;
|
|
1965
|
+
if (env?.executeToolCall) {
|
|
1966
|
+
toolResult = await env.executeToolCall(call, tool, async () => {
|
|
1967
|
+
const r = await executor.processToolWithConfirmation(call, this.ctx, executableTools);
|
|
1968
|
+
return r.result;
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
else {
|
|
1972
|
+
const r = await executor.processToolWithConfirmation(call, this.ctx, executableTools);
|
|
1973
|
+
toolResult = r.result;
|
|
1974
|
+
}
|
|
1975
|
+
results.push(toolResult);
|
|
1929
1976
|
const completedAt = timestamp();
|
|
1930
1977
|
this.emitEvent({
|
|
1931
1978
|
type: "tool_result",
|
|
1932
|
-
callId:
|
|
1933
|
-
name:
|
|
1934
|
-
result:
|
|
1935
|
-
isError: !
|
|
1979
|
+
callId: toolResult.toolUseId,
|
|
1980
|
+
name: toolResult.name,
|
|
1981
|
+
result: toolResult,
|
|
1982
|
+
isError: !toolResult.success,
|
|
1936
1983
|
executedBy: "engine",
|
|
1937
1984
|
startedAt,
|
|
1938
1985
|
completedAt,
|
|
@@ -1987,9 +2034,11 @@ export class SessionImpl extends EventEmitter {
|
|
|
1987
2034
|
const userEntries = [];
|
|
1988
2035
|
const newUserEntries = [];
|
|
1989
2036
|
for (const queued of queuedMessages) {
|
|
1990
|
-
if (
|
|
2037
|
+
if (!queued.content)
|
|
1991
2038
|
continue;
|
|
1992
2039
|
const message = queued.content;
|
|
2040
|
+
if (message.role !== "user")
|
|
2041
|
+
continue;
|
|
1993
2042
|
const existing = existingByMessage.get(message);
|
|
1994
2043
|
if (existing) {
|
|
1995
2044
|
userEntries.push(existing);
|
|
@@ -2029,17 +2078,19 @@ export class SessionImpl extends EventEmitter {
|
|
|
2029
2078
|
toolCalls: response.toolCalls,
|
|
2030
2079
|
toolResults,
|
|
2031
2080
|
};
|
|
2032
|
-
// Add entries to COM - user entries first, then assistant response
|
|
2081
|
+
// Add entries to COM and session timeline - user entries first, then assistant response
|
|
2033
2082
|
for (const entry of newUserEntries) {
|
|
2034
2083
|
this.ctx.addTimelineEntry(entry);
|
|
2084
|
+
this._timeline.push(entry);
|
|
2035
2085
|
}
|
|
2036
2086
|
if (response.newTimelineEntries) {
|
|
2037
2087
|
for (const entry of response.newTimelineEntries) {
|
|
2038
2088
|
this.ctx.addTimelineEntry(entry);
|
|
2089
|
+
this._timeline.push(entry);
|
|
2039
2090
|
}
|
|
2040
2091
|
}
|
|
2041
2092
|
if (toolResults.length > 0) {
|
|
2042
|
-
|
|
2093
|
+
const toolResultEntry = {
|
|
2043
2094
|
kind: "message",
|
|
2044
2095
|
message: {
|
|
2045
2096
|
role: "tool",
|
|
@@ -2052,7 +2103,15 @@ export class SessionImpl extends EventEmitter {
|
|
|
2052
2103
|
})),
|
|
2053
2104
|
},
|
|
2054
2105
|
tags: ["tool_output"],
|
|
2055
|
-
}
|
|
2106
|
+
};
|
|
2107
|
+
this.ctx.addTimelineEntry(toolResultEntry);
|
|
2108
|
+
this._timeline.push(toolResultEntry);
|
|
2109
|
+
}
|
|
2110
|
+
// Apply maxTimelineEntries trim
|
|
2111
|
+
if (this._maxTimelineEntries && this._timeline.length > this._maxTimelineEntries) {
|
|
2112
|
+
const removed = this._timeline.length - this._maxTimelineEntries;
|
|
2113
|
+
this._timeline = this._timeline.slice(-this._maxTimelineEntries);
|
|
2114
|
+
this.log.debug({ removed, remaining: this._timeline.length }, "Timeline trimmed (maxTimelineEntries)");
|
|
2056
2115
|
}
|
|
2057
2116
|
this._currentOutput = current;
|
|
2058
2117
|
// Resolve tick control
|
|
@@ -2067,58 +2126,17 @@ export class SessionImpl extends EventEmitter {
|
|
|
2067
2126
|
/**
|
|
2068
2127
|
* Complete execution and return final state.
|
|
2069
2128
|
*
|
|
2070
|
-
*
|
|
2071
|
-
* - What was sent to the model (from compiled JSX via _lastSentTimeline)
|
|
2072
|
-
* - What the model responded (from _currentOutput.timeline - only NEW entries)
|
|
2073
|
-
*
|
|
2074
|
-
* This ensures the full conversation history is preserved across executions.
|
|
2129
|
+
* Session._timeline is the source of truth. No merge/dedup logic needed.
|
|
2075
2130
|
*/
|
|
2076
2131
|
async complete() {
|
|
2077
2132
|
if (!this.ctx || !this.structureRenderer || !this.compiler) {
|
|
2078
2133
|
throw new Error("Compilation infrastructure not initialized");
|
|
2079
2134
|
}
|
|
2080
|
-
// Build complete timeline:
|
|
2081
|
-
// 1. _lastSentTimeline = what was sent to model (includes history rendered by JSX)
|
|
2082
|
-
// 2. _currentOutput.timeline = new entries from this tick (user input + model response + tool results)
|
|
2083
|
-
//
|
|
2084
|
-
// Only USER messages might overlap (rendered via JSX AND in queuedMessages).
|
|
2085
|
-
// Model responses (assistant/tool) are ALWAYS new and should NEVER be deduplicated.
|
|
2086
|
-
//
|
|
2087
|
-
// NOTE: We use content-based comparison for user messages because <Message {...entry.message}>
|
|
2088
|
-
// creates NEW message objects when rendering history, so reference equality fails.
|
|
2089
|
-
const sentTimeline = this._lastSentTimeline ?? [];
|
|
2090
|
-
const currentTimeline = this._currentOutput?.timeline ?? [];
|
|
2091
|
-
// Create a signature for user messages only
|
|
2092
|
-
const userMessageSignature = (entry) => {
|
|
2093
|
-
if (!entry.message || entry.message.role !== "user")
|
|
2094
|
-
return null;
|
|
2095
|
-
const m = entry.message;
|
|
2096
|
-
if (m.id)
|
|
2097
|
-
return `id:${m.id}`;
|
|
2098
|
-
const contentStr = JSON.stringify(m.content);
|
|
2099
|
-
return `user:${contentStr}`;
|
|
2100
|
-
};
|
|
2101
|
-
// Only track signatures of USER messages that were sent
|
|
2102
|
-
const sentUserSignatures = new Set(sentTimeline.map(userMessageSignature).filter((s) => s !== null));
|
|
2103
|
-
// Filter currentTimeline:
|
|
2104
|
-
// - USER messages: dedupe against sentTimeline (might be rendered via JSX)
|
|
2105
|
-
// - Other roles (assistant, tool): always include (they're new from this tick)
|
|
2106
|
-
const newEntries = currentTimeline.filter((entry) => {
|
|
2107
|
-
if (!entry.message)
|
|
2108
|
-
return true; // Non-message entries always included
|
|
2109
|
-
if (entry.message.role !== "user")
|
|
2110
|
-
return true; // Assistant/tool always included
|
|
2111
|
-
// User message: check for duplicates
|
|
2112
|
-
const sig = userMessageSignature(entry);
|
|
2113
|
-
return sig === null || !sentUserSignatures.has(sig);
|
|
2114
|
-
});
|
|
2115
|
-
const timeline = [...sentTimeline, ...newEntries];
|
|
2116
2135
|
const comOutput = this.ctx.toInput();
|
|
2117
2136
|
const finalOutput = {
|
|
2118
2137
|
...comOutput,
|
|
2119
|
-
timeline,
|
|
2138
|
+
timeline: [...this._timeline],
|
|
2120
2139
|
};
|
|
2121
|
-
// Notify compiler of completion
|
|
2122
2140
|
try {
|
|
2123
2141
|
await this.compiler.notifyComplete(finalOutput);
|
|
2124
2142
|
}
|