@noetic-tools/core 0.0.1 → 0.1.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/dist/adapters/in-memory-fs-adapter.js +1 -1
- package/dist/adapters/in-memory-subprocess/manifest-persistence.d.ts +8 -0
- package/dist/adapters/in-memory-subprocess/manifest-persistence.js +56 -0
- package/dist/adapters/in-memory-subprocess/metadata.d.ts +12 -0
- package/dist/adapters/in-memory-subprocess/metadata.js +58 -0
- package/dist/adapters/in-memory-subprocess/step-completion.d.ts +3 -0
- package/dist/adapters/in-memory-subprocess/step-completion.js +86 -0
- package/dist/adapters/in-memory-subprocess/types.d.ts +88 -0
- package/dist/adapters/in-memory-subprocess/types.js +2 -0
- package/dist/adapters/in-memory-subprocess-adapter.d.ts +12 -4
- package/dist/adapters/in-memory-subprocess-adapter.js +164 -55
- package/dist/adapters/openrouter.d.ts +11 -2
- package/dist/adapters/openrouter.js +66 -5
- package/dist/builders/control-flow-builders.js +8 -3
- package/dist/builders/every.js +4 -1
- package/dist/builders/layer-provides-builders.d.ts +1 -32
- package/dist/builders/layer-provides-builders.js +1 -33
- package/dist/builders/loop-builder.js +4 -1
- package/dist/builders/memory-builder.js +1 -1
- package/dist/builders/provide-builder.js +4 -1
- package/dist/builders/spawn-builder.d.ts +20 -7
- package/dist/builders/spawn-builder.js +16 -2
- package/dist/builders/step-builders.d.ts +49 -28
- package/dist/builders/step-builders.js +30 -11
- package/dist/builders/tool-builder.d.ts +5 -1
- package/dist/builders/tool-builder.js +2 -0
- package/dist/builders/workflow-hydrator.d.ts +38 -0
- package/dist/builders/workflow-hydrator.js +336 -0
- package/dist/conditions/conditions.js +1 -1
- package/dist/errors/noetic-error.js +2 -0
- package/dist/{runtime → harness}/agent-harness.d.ts +65 -45
- package/dist/harness/agent-harness.js +576 -0
- package/dist/harness/deps/adapters.d.ts +4 -0
- package/dist/harness/deps/adapters.js +4 -0
- package/dist/harness/deps/interpreter.d.ts +5 -0
- package/dist/harness/deps/interpreter.js +4 -0
- package/dist/harness/deps/memory.d.ts +5 -0
- package/dist/harness/deps/memory.js +4 -0
- package/dist/harness/deps/runtime.d.ts +10 -0
- package/dist/harness/deps/runtime.js +7 -0
- package/dist/harness/deps/types.d.ts +17 -0
- package/dist/harness/deps/types.js +1 -0
- package/dist/harness/model-call.d.ts +50 -0
- package/dist/harness/model-call.js +694 -0
- package/dist/harness/model-schema.d.ts +19 -0
- package/dist/harness/model-schema.js +46 -0
- package/dist/harness/step-bootstrap.d.ts +25 -0
- package/dist/harness/step-bootstrap.js +170 -0
- package/dist/index.d.ts +48 -12
- package/dist/index.js +29 -7
- package/dist/interpreter/action-deps.d.ts +12 -0
- package/dist/interpreter/action-deps.js +10 -0
- package/dist/interpreter/action-types.d.ts +8 -0
- package/dist/interpreter/action-types.js +1 -0
- package/dist/interpreter/collect-tools.d.ts +1 -1
- package/dist/interpreter/collect-tools.js +6 -2
- package/dist/interpreter/execute-action.d.ts +22 -0
- package/dist/interpreter/execute-action.js +527 -0
- package/dist/interpreter/{execute-every.d.ts → execute-control.d.ts} +7 -1
- package/dist/interpreter/execute-control.js +530 -0
- package/dist/interpreter/execute.d.ts +13 -0
- package/dist/interpreter/execute.js +151 -48
- package/dist/interpreter/message-helpers.d.ts +1 -9
- package/dist/interpreter/message-helpers.js +1 -42
- package/dist/interpreter/step-dispatcher.d.ts +61 -0
- package/dist/interpreter/step-dispatcher.js +67 -0
- package/dist/interpreter/typeguards.d.ts +3 -10
- package/dist/interpreter/typeguards.js +3 -20
- package/dist/{runtime → memory}/exec-context-factory.js +1 -1
- package/dist/memory/flow-schema.d.ts +52 -0
- package/dist/memory/flow-schema.js +102 -0
- package/dist/memory/function-call-utils.js +4 -2
- package/dist/memory/layer-api.d.ts +1 -1
- package/dist/memory/layer-api.js +2 -2
- package/dist/memory/layer-lifecycle.d.ts +1 -1
- package/dist/memory/layer-lifecycle.js +2 -2
- package/dist/memory/layer-provides.d.ts +32 -0
- package/dist/memory/layer-provides.js +33 -0
- package/dist/memory/layer-usage.d.ts +3 -2
- package/dist/memory/layer-usage.js +9 -3
- package/dist/memory/layers/durable-task-state.js +1 -1
- package/dist/memory/layers/file-reference.js +302 -246
- package/dist/memory/layers/history-window.js +1 -1
- package/dist/memory/layers/observational-memory.js +1 -2
- package/dist/memory/layers/plan.d.ts +1 -1
- package/dist/memory/layers/plan.js +13 -8
- package/dist/memory/layers/steering.js +1 -1
- package/dist/memory/layers/tool-memory-layer.d.ts +1 -1
- package/dist/memory/layers/working-memory.js +2 -2
- package/dist/patterns/dynamic-workflow.d.ts +55 -0
- package/dist/patterns/dynamic-workflow.js +194 -0
- package/dist/patterns/flow.d.ts +2 -52
- package/dist/patterns/flow.js +1 -103
- package/dist/patterns/interview.js +1 -1
- package/dist/patterns/plans.d.ts +3 -3
- package/dist/patterns/plans.js +3 -3
- package/dist/patterns/ralph-wiggum.d.ts +1 -1
- package/dist/patterns/react.d.ts +1 -1
- package/dist/portable/adapters.d.ts +6 -0
- package/dist/portable/adapters.js +6 -0
- package/dist/portable/ask-user.d.ts +4 -0
- package/dist/portable/ask-user.js +2 -0
- package/dist/portable/builders.d.ts +12 -0
- package/dist/portable/builders.js +12 -0
- package/dist/portable/memory-layers.d.ts +12 -0
- package/dist/portable/memory-layers.js +12 -0
- package/dist/portable/runtime.d.ts +4 -0
- package/dist/portable/runtime.js +4 -0
- package/dist/portable/until.d.ts +4 -0
- package/dist/portable/until.js +4 -0
- package/dist/portable.d.ts +8 -26
- package/dist/portable.js +8 -34
- package/dist/runtime/broadcaster-utils.d.ts +5 -1
- package/dist/runtime/broadcaster-utils.js +11 -6
- package/dist/runtime/channel-store.js +1 -1
- package/dist/runtime/context-deps.d.ts +4 -0
- package/dist/runtime/context-deps.js +2 -0
- package/dist/runtime/context-impl.d.ts +42 -12
- package/dist/runtime/context-impl.js +51 -4
- package/dist/runtime/context-type-deps.d.ts +14 -0
- package/dist/runtime/context-type-deps.js +1 -0
- package/dist/runtime/detached-handle.d.ts +50 -2
- package/dist/runtime/detached-handle.js +216 -12
- package/dist/runtime/durable/checkpoint-store.d.ts +38 -0
- package/dist/runtime/durable/checkpoint-store.js +101 -0
- package/dist/runtime/durable/detached-signal.d.ts +37 -0
- package/dist/runtime/durable/detached-signal.js +52 -0
- package/dist/runtime/durable/harness-checkpoints.d.ts +51 -0
- package/dist/runtime/durable/harness-checkpoints.js +104 -0
- package/dist/runtime/durable/index.d.ts +10 -0
- package/dist/runtime/durable/index.js +5 -0
- package/dist/runtime/durable/runnable-loop.d.ts +100 -0
- package/dist/runtime/durable/runnable-loop.js +90 -0
- package/dist/runtime/durable/session-seed.d.ts +28 -0
- package/dist/runtime/durable/session-seed.js +27 -0
- package/dist/runtime/durable/stall-nudge.d.ts +82 -0
- package/dist/runtime/durable/stall-nudge.js +87 -0
- package/dist/runtime/in-memory-storage.js +1 -1
- package/dist/runtime/item-log-impl.d.ts +1 -1
- package/dist/runtime/step-registry.d.ts +42 -0
- package/dist/runtime/step-registry.js +77 -0
- package/dist/schemas/workflow.d.ts +139 -0
- package/dist/schemas/workflow.js +232 -0
- package/dist/types/ask-user-types.d.ts +57 -0
- package/dist/types/ask-user-types.js +74 -0
- package/dist/types/checkpoint.d.ts +98 -0
- package/dist/types/checkpoint.js +81 -0
- package/dist/types/common.d.ts +1 -50
- package/dist/types/context-parts/item-log.d.ts +6 -0
- package/dist/types/context-parts/item-log.js +1 -0
- package/dist/types/context-parts/layer-usage.d.ts +18 -0
- package/dist/types/context-parts/layer-usage.js +1 -0
- package/dist/types/context.d.ts +135 -26
- package/dist/types/error.d.ts +15 -0
- package/dist/types/harness-result.d.ts +1 -1
- package/dist/types/items.d.ts +18 -2
- package/dist/types/memory-context.d.ts +39 -0
- package/dist/types/memory-context.js +1 -0
- package/dist/types/memory.d.ts +35 -65
- package/dist/types/runtime.d.ts +2 -1
- package/dist/types/steering.d.ts +1 -1
- package/dist/types/step-registrar.d.ts +46 -0
- package/dist/types/step-registrar.js +56 -0
- package/dist/types/step.d.ts +30 -5
- package/dist/types/subprocess-adapter.d.ts +119 -3
- package/dist/types/subprocess-adapter.js +2 -0
- package/dist/types/tool.d.ts +64 -0
- package/dist/types/tool.js +1 -0
- package/dist/unstable.d.ts +2 -2
- package/dist/unstable.js +2 -2
- package/dist/until/predicates.d.ts +9 -0
- package/dist/until/predicates.js +14 -0
- package/dist/util/message-helpers.d.ts +17 -0
- package/dist/util/message-helpers.js +64 -0
- package/package.json +1 -1
- package/dist/adapters/local-fs-adapter.d.ts +0 -3
- package/dist/adapters/local-fs-adapter.js +0 -43
- package/dist/adapters/local-shell-adapter.d.ts +0 -51
- package/dist/adapters/local-shell-adapter.js +0 -167
- package/dist/adapters/local-subprocess-adapter.d.ts +0 -16
- package/dist/adapters/local-subprocess-adapter.js +0 -187
- package/dist/interpreter/execute-branch.d.ts +0 -3
- package/dist/interpreter/execute-branch.js +0 -10
- package/dist/interpreter/execute-every.js +0 -143
- package/dist/interpreter/execute-fork.d.ts +0 -3
- package/dist/interpreter/execute-fork.js +0 -215
- package/dist/interpreter/execute-llm.d.ts +0 -4
- package/dist/interpreter/execute-llm.js +0 -217
- package/dist/interpreter/execute-loop.d.ts +0 -3
- package/dist/interpreter/execute-loop.js +0 -171
- package/dist/interpreter/execute-provide.d.ts +0 -10
- package/dist/interpreter/execute-provide.js +0 -64
- package/dist/interpreter/execute-run.d.ts +0 -3
- package/dist/interpreter/execute-run.js +0 -39
- package/dist/interpreter/execute-spawn.d.ts +0 -11
- package/dist/interpreter/execute-spawn.js +0 -113
- package/dist/interpreter/execute-tool.d.ts +0 -5
- package/dist/interpreter/execute-tool.js +0 -81
- package/dist/runtime/agent-harness.js +0 -1170
- /package/dist/{runtime → memory}/exec-context-factory.d.ts +0 -0
- /package/dist/{interpreter → util}/framework-cast.d.ts +0 -0
- /package/dist/{interpreter → util}/framework-cast.js +0 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { NoeticConfigError } from '../../errors/noetic-config-error';
|
|
2
|
+
import { CheckpointSnapshotSchema } from '../../types/checkpoint';
|
|
3
|
+
//#region Keys
|
|
4
|
+
/**
|
|
5
|
+
* Typed key constants for the checkpoint namespace. Every execution snapshot
|
|
6
|
+
* lives under a single key so `save`/`load` are cheap single `get`/`set`
|
|
7
|
+
* calls, while a `list` prefix enumeration exposes every known execution id
|
|
8
|
+
* to the restart/reattach path. Auxiliary suffixes exist so future sharded
|
|
9
|
+
* storage schemes can still co-locate snapshot fragments under a shared
|
|
10
|
+
* execution prefix without bumping schemaVersion.
|
|
11
|
+
*
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
const EXEC_KEY_PREFIX = 'execution:';
|
|
15
|
+
const SNAPSHOT_SUFFIX = ':snapshot';
|
|
16
|
+
const FRONTIER_SUFFIX = ':frontier';
|
|
17
|
+
const LAYERS_SUFFIX = ':layers:';
|
|
18
|
+
const CWD_SUFFIX = ':cwd';
|
|
19
|
+
const ASK_USER_SUFFIX = ':askUser';
|
|
20
|
+
const ITEM_LOG_SUFFIX = ':itemLog';
|
|
21
|
+
export const CheckpointKeys = {
|
|
22
|
+
ExecPrefix: EXEC_KEY_PREFIX,
|
|
23
|
+
SnapshotSuffix: SNAPSHOT_SUFFIX,
|
|
24
|
+
FrontierSuffix: FRONTIER_SUFFIX,
|
|
25
|
+
LayersSuffix: LAYERS_SUFFIX,
|
|
26
|
+
CwdSuffix: CWD_SUFFIX,
|
|
27
|
+
AskUserSuffix: ASK_USER_SUFFIX,
|
|
28
|
+
ItemLogSuffix: ITEM_LOG_SUFFIX,
|
|
29
|
+
};
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region Helpers
|
|
32
|
+
function snapshotKey(executionId) {
|
|
33
|
+
return `${EXEC_KEY_PREFIX}${executionId}${SNAPSHOT_SUFFIX}`;
|
|
34
|
+
}
|
|
35
|
+
function executionIdFromSnapshotKey(key) {
|
|
36
|
+
if (!key.startsWith(EXEC_KEY_PREFIX) || !key.endsWith(SNAPSHOT_SUFFIX)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const start = EXEC_KEY_PREFIX.length;
|
|
40
|
+
const end = key.length - SNAPSHOT_SUFFIX.length;
|
|
41
|
+
if (end <= start) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return key.slice(start, end);
|
|
45
|
+
}
|
|
46
|
+
function isJsonRecord(value) {
|
|
47
|
+
return typeof value === 'object' && value !== null;
|
|
48
|
+
}
|
|
49
|
+
function parseSnapshot(raw, executionId) {
|
|
50
|
+
const parsed = CheckpointSnapshotSchema.safeParse(raw);
|
|
51
|
+
if (parsed.success) {
|
|
52
|
+
return parsed.data;
|
|
53
|
+
}
|
|
54
|
+
const observedVersion = isJsonRecord(raw) && 'schemaVersion' in raw ? raw.schemaVersion : 'unknown';
|
|
55
|
+
throw new NoeticConfigError({
|
|
56
|
+
code: 'CHECKPOINT_SCHEMA_MISMATCH',
|
|
57
|
+
message: `Checkpoint snapshot for execution "${executionId}" failed schema validation (observed schemaVersion=${String(observedVersion)}).`,
|
|
58
|
+
hint: 'The snapshot was produced by a different runtime version. Clear the snapshot via CheckpointStore.clear() and restart the execution.',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region Factory
|
|
63
|
+
export function createCheckpointStore(options) {
|
|
64
|
+
const { storage } = options;
|
|
65
|
+
async function save(snapshot) {
|
|
66
|
+
// Persisted shape is the validated snapshot. The adapter is free to
|
|
67
|
+
// JSON-encode it internally; `InMemoryStorage` stores by reference which
|
|
68
|
+
// is fine because `CheckpointSnapshotSchema.parse` is deep-immutable.
|
|
69
|
+
await storage.set(snapshotKey(snapshot.executionId), snapshot);
|
|
70
|
+
}
|
|
71
|
+
async function load(executionId) {
|
|
72
|
+
const raw = await storage.get(snapshotKey(executionId));
|
|
73
|
+
if (raw === null) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
return parseSnapshot(raw, executionId);
|
|
77
|
+
}
|
|
78
|
+
async function list() {
|
|
79
|
+
const keys = await storage.list(EXEC_KEY_PREFIX);
|
|
80
|
+
const out = [];
|
|
81
|
+
for (const key of keys) {
|
|
82
|
+
const id = executionIdFromSnapshotKey(key);
|
|
83
|
+
if (id !== null) {
|
|
84
|
+
out.push({
|
|
85
|
+
executionId: id,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
async function clear(executionId) {
|
|
92
|
+
await storage.delete(snapshotKey(executionId));
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
save,
|
|
96
|
+
load,
|
|
97
|
+
list,
|
|
98
|
+
clear,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deferred-style, single-shot signal used to wire a detached child — a
|
|
3
|
+
* terminal tool, an IPC handler, an external watchdog — back to the
|
|
4
|
+
* parent that awaits its outcome.
|
|
5
|
+
*
|
|
6
|
+
* The pattern is intentionally minimal: the parent awaits `signal.done`
|
|
7
|
+
* while the child calls `signal.resolve(outcome)` (or `signal.reject(err)`)
|
|
8
|
+
* when it has produced the final value. Any subsequent `resolve` /
|
|
9
|
+
* `reject` calls are silently dropped so a late-arriving duplicate
|
|
10
|
+
* settlement never overwrites the first.
|
|
11
|
+
*
|
|
12
|
+
* This is the canonical primitive the `runnableLoop` (and the task
|
|
13
|
+
* runner scripts that use it) build on. Keeping it in core means every
|
|
14
|
+
* out-of-process worker / detached step shares one signal shape rather
|
|
15
|
+
* than each reinventing the single-shot-deferred.
|
|
16
|
+
*/
|
|
17
|
+
/** Single-shot deferred signal the caller awaits via `.done`. */
|
|
18
|
+
export interface DetachedSignal<TOutcome> {
|
|
19
|
+
readonly done: Promise<TOutcome>;
|
|
20
|
+
/**
|
|
21
|
+
* Settle the signal with a success outcome. Only the first call has
|
|
22
|
+
* effect; subsequent calls (from retry logic, duplicate terminal
|
|
23
|
+
* tool invocations, external signal handlers) are dropped silently.
|
|
24
|
+
*/
|
|
25
|
+
resolve(outcome: TOutcome): void;
|
|
26
|
+
/**
|
|
27
|
+
* Settle the signal with a failure. Only the first call has effect;
|
|
28
|
+
* a `resolve` that races with a `reject` wins by whichever runs
|
|
29
|
+
* first — both further attempts are dropped.
|
|
30
|
+
*/
|
|
31
|
+
reject(err: unknown): void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Construct a fresh single-shot `DetachedSignal<TOutcome>`. The returned
|
|
35
|
+
* signal's `done` Promise resolves/rejects at most once.
|
|
36
|
+
*/
|
|
37
|
+
export declare function createDetachedSignal<TOutcome>(): DetachedSignal<TOutcome>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deferred-style, single-shot signal used to wire a detached child — a
|
|
3
|
+
* terminal tool, an IPC handler, an external watchdog — back to the
|
|
4
|
+
* parent that awaits its outcome.
|
|
5
|
+
*
|
|
6
|
+
* The pattern is intentionally minimal: the parent awaits `signal.done`
|
|
7
|
+
* while the child calls `signal.resolve(outcome)` (or `signal.reject(err)`)
|
|
8
|
+
* when it has produced the final value. Any subsequent `resolve` /
|
|
9
|
+
* `reject` calls are silently dropped so a late-arriving duplicate
|
|
10
|
+
* settlement never overwrites the first.
|
|
11
|
+
*
|
|
12
|
+
* This is the canonical primitive the `runnableLoop` (and the task
|
|
13
|
+
* runner scripts that use it) build on. Keeping it in core means every
|
|
14
|
+
* out-of-process worker / detached step shares one signal shape rather
|
|
15
|
+
* than each reinventing the single-shot-deferred.
|
|
16
|
+
*/
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region Public API
|
|
19
|
+
/**
|
|
20
|
+
* Construct a fresh single-shot `DetachedSignal<TOutcome>`. The returned
|
|
21
|
+
* signal's `done` Promise resolves/rejects at most once.
|
|
22
|
+
*/
|
|
23
|
+
export function createDetachedSignal() {
|
|
24
|
+
let resolveFn = null;
|
|
25
|
+
let rejectFn = null;
|
|
26
|
+
const done = new Promise((resolve, reject) => {
|
|
27
|
+
resolveFn = resolve;
|
|
28
|
+
rejectFn = reject;
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
done,
|
|
32
|
+
resolve(outcome) {
|
|
33
|
+
if (resolveFn === null) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const fn = resolveFn;
|
|
37
|
+
resolveFn = null;
|
|
38
|
+
rejectFn = null;
|
|
39
|
+
fn(outcome);
|
|
40
|
+
},
|
|
41
|
+
reject(err) {
|
|
42
|
+
if (rejectFn === null) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const fn = rejectFn;
|
|
46
|
+
resolveFn = null;
|
|
47
|
+
rejectFn = null;
|
|
48
|
+
fn(err);
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { LayerStateStore } from '../../memory/layer-lifecycle';
|
|
2
|
+
import type { ItemSchemaRegistry } from '../../schemas/item';
|
|
3
|
+
import type { Context } from '../../types/context';
|
|
4
|
+
import type { Item } from '../../types/items';
|
|
5
|
+
import type { MemoryLayer } from '../../types/memory';
|
|
6
|
+
import type { CheckpointStore } from './checkpoint-store';
|
|
7
|
+
/**
|
|
8
|
+
* Minimum harness surface `captureCheckpoint` / `restoreFromCheckpoint`
|
|
9
|
+
* require. `AgentHarness` already satisfies this shape via its
|
|
10
|
+
* `@internal` readonly fields — defining it structurally keeps the free
|
|
11
|
+
* functions loosely coupled to the harness implementation.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export interface CheckpointHarnessHandle {
|
|
16
|
+
readonly checkpointStore?: CheckpointStore;
|
|
17
|
+
readonly layerStateStore: LayerStateStore;
|
|
18
|
+
readonly itemSchemas: ItemSchemaRegistry;
|
|
19
|
+
readonly _memory?: MemoryLayer[];
|
|
20
|
+
createContext(opts?: {
|
|
21
|
+
items?: Item[];
|
|
22
|
+
threadId?: string;
|
|
23
|
+
resourceId?: string;
|
|
24
|
+
cwdInit?: string;
|
|
25
|
+
memory?: MemoryLayer[];
|
|
26
|
+
}): Context;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Snapshot the execution state at a checkpoint boundary. No-op when no
|
|
30
|
+
* `CheckpointStore` is configured — zero-config harnesses preserve
|
|
31
|
+
* ephemeral semantics. Save failures are logged rather than thrown,
|
|
32
|
+
* because a checkpoint failing must never abort an otherwise-successful
|
|
33
|
+
* step.
|
|
34
|
+
*
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
export declare function captureCheckpoint(h: CheckpointHarnessHandle, ctx: Context): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Rebuild a `Context` from a previously-persisted snapshot. Returns
|
|
40
|
+
* `null` if no snapshot is recorded for `executionId`. Layer state is
|
|
41
|
+
* replayed into `layerStateStore` keyed by the original executionId so
|
|
42
|
+
* the restored context observes the pre-crash state through
|
|
43
|
+
* `readLayerState` and the memory projectors.
|
|
44
|
+
*
|
|
45
|
+
* Preserves the original executionId on the returned context via
|
|
46
|
+
* `Object.defineProperty` — adapter correlation across crash/restart
|
|
47
|
+
* requires a stable id.
|
|
48
|
+
*
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
51
|
+
export declare function restoreFromCheckpoint(h: CheckpointHarnessHandle, executionId: string): Promise<Context | null>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { CheckpointSchemaVersion } from '../../types/checkpoint';
|
|
2
|
+
import { ContextImpl } from '../context-impl';
|
|
3
|
+
//#endregion
|
|
4
|
+
//#region captureCheckpoint
|
|
5
|
+
/**
|
|
6
|
+
* Snapshot the execution state at a checkpoint boundary. No-op when no
|
|
7
|
+
* `CheckpointStore` is configured — zero-config harnesses preserve
|
|
8
|
+
* ephemeral semantics. Save failures are logged rather than thrown,
|
|
9
|
+
* because a checkpoint failing must never abort an otherwise-successful
|
|
10
|
+
* step.
|
|
11
|
+
*
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
export async function captureCheckpoint(h, ctx) {
|
|
15
|
+
const store = h.checkpointStore;
|
|
16
|
+
if (!store) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const impl = ctx instanceof ContextImpl ? ctx : null;
|
|
20
|
+
const frontier = impl ? impl.serialiseFrontier() : [];
|
|
21
|
+
const layers = {};
|
|
22
|
+
for (const layer of ctx.layers ?? []) {
|
|
23
|
+
const state = h.layerStateStore.get(ctx.id, layer.id);
|
|
24
|
+
if (state !== undefined) {
|
|
25
|
+
layers[layer.id] = state;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const snapshot = {
|
|
29
|
+
schemaVersion: CheckpointSchemaVersion,
|
|
30
|
+
executionId: ctx.id,
|
|
31
|
+
threadId: ctx.threadId,
|
|
32
|
+
resourceId: ctx.resourceId,
|
|
33
|
+
frontier,
|
|
34
|
+
layers,
|
|
35
|
+
cwd: {
|
|
36
|
+
current: ctx.cwdState.cwd,
|
|
37
|
+
previous: ctx.cwdState.previousCwd,
|
|
38
|
+
},
|
|
39
|
+
// Ask-user queue snapshot is empty at the core layer — the code-agent
|
|
40
|
+
// host is responsible for pushing pending prompts through this store
|
|
41
|
+
// via `AskUserService` integration. Carrying the shape from day one
|
|
42
|
+
// means future producers don't bump the schema version.
|
|
43
|
+
askUser: [],
|
|
44
|
+
itemLog: {
|
|
45
|
+
items: [
|
|
46
|
+
...ctx.itemLog.items,
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
capturedAt: new Date().toISOString(),
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
await store.save(snapshot);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
console.warn(`AgentHarness.checkpoint: failed to persist snapshot for execution "${ctx.id}":`, err);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region restoreFromCheckpoint
|
|
60
|
+
/**
|
|
61
|
+
* Rebuild a `Context` from a previously-persisted snapshot. Returns
|
|
62
|
+
* `null` if no snapshot is recorded for `executionId`. Layer state is
|
|
63
|
+
* replayed into `layerStateStore` keyed by the original executionId so
|
|
64
|
+
* the restored context observes the pre-crash state through
|
|
65
|
+
* `readLayerState` and the memory projectors.
|
|
66
|
+
*
|
|
67
|
+
* Preserves the original executionId on the returned context via
|
|
68
|
+
* `Object.defineProperty` — adapter correlation across crash/restart
|
|
69
|
+
* requires a stable id.
|
|
70
|
+
*
|
|
71
|
+
* @internal
|
|
72
|
+
*/
|
|
73
|
+
export async function restoreFromCheckpoint(h, executionId) {
|
|
74
|
+
const store = h.checkpointStore;
|
|
75
|
+
if (!store) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const snapshot = await store.load(executionId);
|
|
79
|
+
if (!snapshot) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
for (const [layerId, state] of Object.entries(snapshot.layers)) {
|
|
83
|
+
h.layerStateStore.set(executionId, layerId, state);
|
|
84
|
+
}
|
|
85
|
+
const items = h.itemSchemas.parseMany(snapshot.itemLog.items);
|
|
86
|
+
const cwdInit = snapshot.cwd?.current ?? undefined;
|
|
87
|
+
const ctx = h.createContext({
|
|
88
|
+
items,
|
|
89
|
+
threadId: snapshot.threadId,
|
|
90
|
+
resourceId: snapshot.resourceId,
|
|
91
|
+
cwdInit,
|
|
92
|
+
memory: h._memory,
|
|
93
|
+
});
|
|
94
|
+
if (ctx instanceof ContextImpl) {
|
|
95
|
+
Object.defineProperty(ctx, 'id', {
|
|
96
|
+
value: executionId,
|
|
97
|
+
configurable: false,
|
|
98
|
+
writable: false,
|
|
99
|
+
enumerable: true,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return ctx;
|
|
103
|
+
}
|
|
104
|
+
//#endregion
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type { CheckpointStore, CreateCheckpointStoreOptions } from './checkpoint-store';
|
|
2
|
+
export { CheckpointKeys, createCheckpointStore } from './checkpoint-store';
|
|
3
|
+
export type { DetachedSignal } from './detached-signal';
|
|
4
|
+
export { createDetachedSignal } from './detached-signal';
|
|
5
|
+
export type { AfterFirstTurnContext, RunnableLoopHarness, RunnableLoopOpts } from './runnable-loop';
|
|
6
|
+
export { runnableLoop } from './runnable-loop';
|
|
7
|
+
export type { SessionSeedHarness } from './session-seed';
|
|
8
|
+
export { seedFromItems } from './session-seed';
|
|
9
|
+
export type { CreateNudgeMessageOpts, StallNudgeOpts } from './stall-nudge';
|
|
10
|
+
export { createNudgeMessage, createStallNudgeHook, DEFAULT_NUDGE_MESSAGE_TEXT, } from './stall-nudge';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { CheckpointKeys, createCheckpointStore } from './checkpoint-store';
|
|
2
|
+
export { createDetachedSignal } from './detached-signal';
|
|
3
|
+
export { runnableLoop } from './runnable-loop';
|
|
4
|
+
export { seedFromItems } from './session-seed';
|
|
5
|
+
export { createNudgeMessage, createStallNudgeHook, DEFAULT_NUDGE_MESSAGE_TEXT, } from './stall-nudge';
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drive a generic agent-harness chat session for a detached runner.
|
|
3
|
+
*
|
|
4
|
+
* The loop is intentionally minimal. A "runnable" is any harness
|
|
5
|
+
* driven through the standard `execute()` path plus a single-shot
|
|
6
|
+
* `DetachedSignal` its terminal tool resolves:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Resume spawn** — when `priorItems` is non-empty, seed the
|
|
9
|
+
* session history and hand control straight to the caller's
|
|
10
|
+
* signal. The agent's prior turns are already on record; the next
|
|
11
|
+
* turn comes from external input (IPC `send` frame, autopilot
|
|
12
|
+
* respawn, etc.) rather than being re-kicked here.
|
|
13
|
+
* 2. **Fresh spawn** — when `priorItems` is empty (or omitted) and
|
|
14
|
+
* an `initialMessage` is supplied, execute that framing message
|
|
15
|
+
* as the first turn. The harness's own stream pumps persist the
|
|
16
|
+
* emitted items via whatever session store the caller wired up.
|
|
17
|
+
* 3. **Post-turn hook** — if `afterFirstTurn` is supplied, run it
|
|
18
|
+
* once the first turn has settled microtasks. The hook sees
|
|
19
|
+
* whether the signal already resolved during the turn so it can
|
|
20
|
+
* short-circuit its own logic (e.g. the stall-nudge helper skips
|
|
21
|
+
* its nudge when the agent already called a terminal tool).
|
|
22
|
+
* 4. **Await outcome** — finally, return `signal.done`. If no
|
|
23
|
+
* terminal tool ever resolves it, the signal rejects externally
|
|
24
|
+
* (SIGTERM handler, process abort) or simply never settles — the
|
|
25
|
+
* caller owns that concern.
|
|
26
|
+
*
|
|
27
|
+
* The loop has no opinions about where `priorItems` come from, how the
|
|
28
|
+
* `initialMessage` is built, or what the terminal outcome looks like.
|
|
29
|
+
* Task-specific logic (chat.jsonl reads, role framing, stall
|
|
30
|
+
* escalation) lives in the caller and composes via the `afterFirstTurn`
|
|
31
|
+
* hook.
|
|
32
|
+
*/
|
|
33
|
+
import type { HarnessResponse } from '../../types/harness-result';
|
|
34
|
+
import type { ExecuteInput, Item } from '../../types/items';
|
|
35
|
+
import type { ExecuteOptions, SessionScope } from '../../types/runtime';
|
|
36
|
+
import type { DetachedSignal } from './detached-signal';
|
|
37
|
+
/**
|
|
38
|
+
* Minimum harness surface the loop calls into. Defining this
|
|
39
|
+
* structurally keeps the loop loosely coupled — the concrete
|
|
40
|
+
* `AgentHarness<P>` already satisfies the shape, as does a test stub
|
|
41
|
+
* that only stands up `seedSessionHistory` + `execute` + `getAgentResponse`.
|
|
42
|
+
*
|
|
43
|
+
* `getAgentResponse` is REQUIRED because `execute()` only enqueues a
|
|
44
|
+
* message and returns before the turn actually runs. The loop awaits
|
|
45
|
+
* `getAgentResponse(scope)` to observe real turn completion before
|
|
46
|
+
* handing to `afterFirstTurn` — without this, stall detection fires
|
|
47
|
+
* before the agent has produced any output and flags every run as
|
|
48
|
+
* stalled.
|
|
49
|
+
*/
|
|
50
|
+
export interface RunnableLoopHarness {
|
|
51
|
+
seedSessionHistory(threadId: string, items: ReadonlyArray<Item>): void;
|
|
52
|
+
execute(input: ExecuteInput, options?: ExecuteOptions): Promise<void>;
|
|
53
|
+
getAgentResponse(scope?: SessionScope): Promise<HarnessResponse>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Read-only view the `afterFirstTurn` hook uses to decide whether the
|
|
57
|
+
* signal settled during the turn it just observed. Consumers call
|
|
58
|
+
* `signalSettled()` (not a direct property read) so a late-settle
|
|
59
|
+
* caused by microtask ordering still propagates.
|
|
60
|
+
*/
|
|
61
|
+
export interface AfterFirstTurnContext {
|
|
62
|
+
/** True if `signal.done` has resolved or rejected. */
|
|
63
|
+
signalSettled(): boolean;
|
|
64
|
+
}
|
|
65
|
+
export interface RunnableLoopOpts<TOutcome> {
|
|
66
|
+
readonly harness: RunnableLoopHarness;
|
|
67
|
+
readonly threadId: string;
|
|
68
|
+
/**
|
|
69
|
+
* Prior session items to seed before the first turn. When non-empty
|
|
70
|
+
* the loop skips `initialMessage` entirely (resume path); when empty
|
|
71
|
+
* or omitted it proceeds with the fresh-spawn sequence.
|
|
72
|
+
*/
|
|
73
|
+
readonly priorItems?: ReadonlyArray<Item>;
|
|
74
|
+
/**
|
|
75
|
+
* Developer-role framing message that kicks off a fresh run. Omit to
|
|
76
|
+
* run the loop in "seed + wait" mode where the first turn is driven
|
|
77
|
+
* externally (e.g. by an IPC client's `send` frame).
|
|
78
|
+
*/
|
|
79
|
+
readonly initialMessage?: ExecuteInput;
|
|
80
|
+
readonly signal: DetachedSignal<TOutcome>;
|
|
81
|
+
/**
|
|
82
|
+
* Optional post-turn hook. Only runs on the fresh-spawn path once
|
|
83
|
+
* `initialMessage` has settled microtasks. Receives a context whose
|
|
84
|
+
* `signalSettled()` query reflects whether the signal is already
|
|
85
|
+
* resolved/rejected so the hook can no-op when the agent already
|
|
86
|
+
* produced its terminal outcome.
|
|
87
|
+
*/
|
|
88
|
+
readonly afterFirstTurn?: (ctx: AfterFirstTurnContext) => Promise<void>;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Execute the runnable-loop lifecycle and return the final outcome by
|
|
92
|
+
* awaiting the caller-provided signal. See the module doc for the full
|
|
93
|
+
* sequence; short version:
|
|
94
|
+
*
|
|
95
|
+
* - `priorItems` present → seed + wait on signal.
|
|
96
|
+
* - `initialMessage` present → seed (if any) + execute first turn +
|
|
97
|
+
* run `afterFirstTurn` (if any) + wait on signal.
|
|
98
|
+
* - Neither present → just wait on signal (pure listener mode).
|
|
99
|
+
*/
|
|
100
|
+
export declare function runnableLoop<TOutcome>(opts: RunnableLoopOpts<TOutcome>): Promise<TOutcome>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drive a generic agent-harness chat session for a detached runner.
|
|
3
|
+
*
|
|
4
|
+
* The loop is intentionally minimal. A "runnable" is any harness
|
|
5
|
+
* driven through the standard `execute()` path plus a single-shot
|
|
6
|
+
* `DetachedSignal` its terminal tool resolves:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Resume spawn** — when `priorItems` is non-empty, seed the
|
|
9
|
+
* session history and hand control straight to the caller's
|
|
10
|
+
* signal. The agent's prior turns are already on record; the next
|
|
11
|
+
* turn comes from external input (IPC `send` frame, autopilot
|
|
12
|
+
* respawn, etc.) rather than being re-kicked here.
|
|
13
|
+
* 2. **Fresh spawn** — when `priorItems` is empty (or omitted) and
|
|
14
|
+
* an `initialMessage` is supplied, execute that framing message
|
|
15
|
+
* as the first turn. The harness's own stream pumps persist the
|
|
16
|
+
* emitted items via whatever session store the caller wired up.
|
|
17
|
+
* 3. **Post-turn hook** — if `afterFirstTurn` is supplied, run it
|
|
18
|
+
* once the first turn has settled microtasks. The hook sees
|
|
19
|
+
* whether the signal already resolved during the turn so it can
|
|
20
|
+
* short-circuit its own logic (e.g. the stall-nudge helper skips
|
|
21
|
+
* its nudge when the agent already called a terminal tool).
|
|
22
|
+
* 4. **Await outcome** — finally, return `signal.done`. If no
|
|
23
|
+
* terminal tool ever resolves it, the signal rejects externally
|
|
24
|
+
* (SIGTERM handler, process abort) or simply never settles — the
|
|
25
|
+
* caller owns that concern.
|
|
26
|
+
*
|
|
27
|
+
* The loop has no opinions about where `priorItems` come from, how the
|
|
28
|
+
* `initialMessage` is built, or what the terminal outcome looks like.
|
|
29
|
+
* Task-specific logic (chat.jsonl reads, role framing, stall
|
|
30
|
+
* escalation) lives in the caller and composes via the `afterFirstTurn`
|
|
31
|
+
* hook.
|
|
32
|
+
*/
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region Public API
|
|
35
|
+
/**
|
|
36
|
+
* Execute the runnable-loop lifecycle and return the final outcome by
|
|
37
|
+
* awaiting the caller-provided signal. See the module doc for the full
|
|
38
|
+
* sequence; short version:
|
|
39
|
+
*
|
|
40
|
+
* - `priorItems` present → seed + wait on signal.
|
|
41
|
+
* - `initialMessage` present → seed (if any) + execute first turn +
|
|
42
|
+
* run `afterFirstTurn` (if any) + wait on signal.
|
|
43
|
+
* - Neither present → just wait on signal (pure listener mode).
|
|
44
|
+
*/
|
|
45
|
+
export async function runnableLoop(opts) {
|
|
46
|
+
const prior = opts.priorItems;
|
|
47
|
+
if (prior !== undefined && prior.length > 0) {
|
|
48
|
+
opts.harness.seedSessionHistory(opts.threadId, prior);
|
|
49
|
+
return opts.signal.done;
|
|
50
|
+
}
|
|
51
|
+
if (opts.initialMessage === undefined) {
|
|
52
|
+
return opts.signal.done;
|
|
53
|
+
}
|
|
54
|
+
// Track signal settlement without blocking so the `afterFirstTurn`
|
|
55
|
+
// hook can detect a synchronous resolve that fired during `execute()`
|
|
56
|
+
// without racing the deferred.
|
|
57
|
+
let signalSettled = false;
|
|
58
|
+
void opts.signal.done.then(() => {
|
|
59
|
+
signalSettled = true;
|
|
60
|
+
}, () => {
|
|
61
|
+
signalSettled = true;
|
|
62
|
+
});
|
|
63
|
+
await opts.harness.execute(opts.initialMessage, {
|
|
64
|
+
threadId: opts.threadId,
|
|
65
|
+
});
|
|
66
|
+
// `execute()` only enqueues the framing message; the turn itself runs
|
|
67
|
+
// asynchronously through the session runner. Await `getAgentResponse`
|
|
68
|
+
// to observe REAL turn completion before dispatching `afterFirstTurn`.
|
|
69
|
+
// Without this the stall-nudge hook fires before any turn output and
|
|
70
|
+
// always classifies the run as stalled. We race against `signal.done`
|
|
71
|
+
// so a terminal tool that resolves the signal mid-turn still short-
|
|
72
|
+
// circuits the wait — in that case `getAgentResponse` may not have
|
|
73
|
+
// settled yet and we skip the post-turn hook entirely since the
|
|
74
|
+
// terminal outcome has already fired.
|
|
75
|
+
await Promise.race([
|
|
76
|
+
opts.harness
|
|
77
|
+
.getAgentResponse({
|
|
78
|
+
threadId: opts.threadId,
|
|
79
|
+
})
|
|
80
|
+
.catch(() => undefined),
|
|
81
|
+
opts.signal.done.catch(() => undefined),
|
|
82
|
+
]);
|
|
83
|
+
if (opts.afterFirstTurn !== undefined) {
|
|
84
|
+
await opts.afterFirstTurn({
|
|
85
|
+
signalSettled: () => signalSettled,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return opts.signal.done;
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seed an agent harness's session history from a pre-parsed `Item[]`.
|
|
3
|
+
*
|
|
4
|
+
* Callers who have chat history on disk (JSONL, DB, etc.) read + parse
|
|
5
|
+
* the persisted format into `Item[]` themselves — this helper is
|
|
6
|
+
* deliberately path-free so it composes with any storage layer.
|
|
7
|
+
*
|
|
8
|
+
* The helper is a thin typed wrapper around `harness.seedSessionHistory`
|
|
9
|
+
* and exists so the runnable-loop (and any other caller that wants to
|
|
10
|
+
* restore a prior session) has one documented, named entrypoint rather
|
|
11
|
+
* than reaching for the harness method inline.
|
|
12
|
+
*/
|
|
13
|
+
import type { Item } from '../../types/items';
|
|
14
|
+
/**
|
|
15
|
+
* Minimum harness surface `seedFromItems` requires. Defined structurally
|
|
16
|
+
* so tests (and out-of-process children) can pass a stub without casts;
|
|
17
|
+
* `AgentHarness<P>` from core satisfies the shape directly.
|
|
18
|
+
*/
|
|
19
|
+
export interface SessionSeedHarness {
|
|
20
|
+
seedSessionHistory(threadId: string, items: ReadonlyArray<Item>): void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Seed `harness`'s session for `threadId` with the given items so the
|
|
24
|
+
* next `execute()` call sees them in the item log. No-op when `items`
|
|
25
|
+
* is empty (the harness call itself is also safe to make with an empty
|
|
26
|
+
* array; short-circuiting here keeps the intent crisp).
|
|
27
|
+
*/
|
|
28
|
+
export declare function seedFromItems(harness: SessionSeedHarness, threadId: string, items: ReadonlyArray<Item>): void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seed an agent harness's session history from a pre-parsed `Item[]`.
|
|
3
|
+
*
|
|
4
|
+
* Callers who have chat history on disk (JSONL, DB, etc.) read + parse
|
|
5
|
+
* the persisted format into `Item[]` themselves — this helper is
|
|
6
|
+
* deliberately path-free so it composes with any storage layer.
|
|
7
|
+
*
|
|
8
|
+
* The helper is a thin typed wrapper around `harness.seedSessionHistory`
|
|
9
|
+
* and exists so the runnable-loop (and any other caller that wants to
|
|
10
|
+
* restore a prior session) has one documented, named entrypoint rather
|
|
11
|
+
* than reaching for the harness method inline.
|
|
12
|
+
*/
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region Public API
|
|
15
|
+
/**
|
|
16
|
+
* Seed `harness`'s session for `threadId` with the given items so the
|
|
17
|
+
* next `execute()` call sees them in the item log. No-op when `items`
|
|
18
|
+
* is empty (the harness call itself is also safe to make with an empty
|
|
19
|
+
* array; short-circuiting here keeps the intent crisp).
|
|
20
|
+
*/
|
|
21
|
+
export function seedFromItems(harness, threadId, items) {
|
|
22
|
+
if (items.length === 0) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
harness.seedSessionHistory(threadId, items);
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|