kernl 0.2.0 → 0.6.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/.turbo/turbo-build.log +4 -5
- package/.turbo/turbo-check-types.log +4 -0
- package/CHANGELOG.md +147 -0
- package/LICENSE +1 -1
- package/dist/agent/__tests__/concurrency.test.d.ts +2 -0
- package/dist/agent/__tests__/concurrency.test.d.ts.map +1 -0
- package/dist/agent/__tests__/concurrency.test.js +152 -0
- package/dist/agent/__tests__/run.test.d.ts +2 -0
- package/dist/agent/__tests__/run.test.d.ts.map +1 -0
- package/dist/agent/__tests__/run.test.js +357 -0
- package/dist/agent/index.d.ts +1 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent.d.ts +32 -9
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +102 -14
- package/dist/api/__tests__/cursor-page.test.d.ts +2 -0
- package/dist/api/__tests__/cursor-page.test.d.ts.map +1 -0
- package/dist/api/__tests__/cursor-page.test.js +414 -0
- package/dist/api/__tests__/offset-page.test.d.ts +2 -0
- package/dist/api/__tests__/offset-page.test.d.ts.map +1 -0
- package/dist/api/__tests__/offset-page.test.js +510 -0
- package/dist/api/__tests__/threads.test.d.ts +2 -0
- package/dist/api/__tests__/threads.test.d.ts.map +1 -0
- package/dist/api/__tests__/threads.test.js +338 -0
- package/dist/api/models/index.d.ts +2 -0
- package/dist/api/models/index.d.ts.map +1 -0
- package/dist/api/models/thread.d.ts +120 -0
- package/dist/api/models/thread.d.ts.map +1 -0
- package/dist/api/pagination/base.d.ts +48 -0
- package/dist/api/pagination/base.d.ts.map +1 -0
- package/dist/api/pagination/base.js +45 -0
- package/dist/api/pagination/cursor.d.ts +44 -0
- package/dist/api/pagination/cursor.d.ts.map +1 -0
- package/dist/api/pagination/cursor.js +52 -0
- package/dist/api/pagination/offset.d.ts +42 -0
- package/dist/api/pagination/offset.d.ts.map +1 -0
- package/dist/api/pagination/offset.js +55 -0
- package/dist/api/resources/threads/events.d.ts +21 -0
- package/dist/api/resources/threads/events.d.ts.map +1 -0
- package/dist/api/resources/threads/events.js +24 -0
- package/dist/api/resources/threads/index.d.ts +4 -0
- package/dist/api/resources/threads/index.d.ts.map +1 -0
- package/dist/api/resources/threads/index.js +2 -0
- package/dist/api/resources/threads/threads.d.ts +57 -0
- package/dist/api/resources/threads/threads.d.ts.map +1 -0
- package/dist/api/resources/threads/threads.js +199 -0
- package/dist/api/resources/threads/types.d.ts +123 -0
- package/dist/api/resources/threads/types.d.ts.map +1 -0
- package/dist/api/resources/threads/utils.d.ts +18 -0
- package/dist/api/resources/threads/utils.d.ts.map +1 -0
- package/dist/api/resources/threads/utils.js +78 -0
- package/dist/context.d.ts +5 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +6 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/internal.d.ts +4 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +2 -0
- package/dist/kernl/index.d.ts +3 -0
- package/dist/kernl/index.d.ts.map +1 -0
- package/dist/kernl/index.js +2 -0
- package/dist/kernl/kernl.d.ts +64 -0
- package/dist/kernl/kernl.d.ts.map +1 -0
- package/dist/kernl/kernl.js +116 -0
- package/dist/kernl/threads.d.ts +110 -0
- package/dist/kernl/threads.d.ts.map +1 -0
- package/dist/kernl/threads.js +126 -0
- package/dist/kernl.d.ts +22 -6
- package/dist/kernl.d.ts.map +1 -1
- package/dist/kernl.js +73 -10
- package/dist/lib/env.d.ts +3 -3
- package/dist/lib/env.js +1 -1
- package/dist/mcp/__tests__/integration.test.js +8 -8
- package/dist/mcp/__tests__/utils.test.js +6 -6
- package/dist/mcp/http.d.ts +1 -1
- package/dist/mcp/http.d.ts.map +1 -1
- package/dist/mcp/http.js +9 -9
- package/dist/mcp/sse.d.ts +1 -1
- package/dist/mcp/sse.d.ts.map +1 -1
- package/dist/mcp/sse.js +7 -7
- package/dist/mcp/utils.d.ts +1 -1
- package/dist/mcp/utils.d.ts.map +1 -1
- package/dist/mcp/utils.js +4 -5
- package/dist/storage/__tests__/in-memory.test.d.ts +2 -0
- package/dist/storage/__tests__/in-memory.test.d.ts.map +1 -0
- package/dist/storage/__tests__/in-memory.test.js +455 -0
- package/dist/storage/base.d.ts +64 -0
- package/dist/storage/base.d.ts.map +1 -0
- package/dist/storage/base.js +4 -0
- package/dist/storage/in-memory.d.ts +62 -0
- package/dist/storage/in-memory.d.ts.map +1 -0
- package/dist/storage/in-memory.js +283 -0
- package/dist/storage/index.d.ts +10 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +7 -0
- package/dist/storage/thread.d.ts +123 -0
- package/dist/storage/thread.d.ts.map +1 -0
- package/dist/storage/thread.js +4 -0
- package/dist/task.d.ts +5 -3
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js +10 -8
- package/dist/thread/__tests__/fixtures/mock-model.d.ts +1 -2
- package/dist/thread/__tests__/fixtures/mock-model.d.ts.map +1 -1
- package/dist/thread/__tests__/integration.test.js +73 -5
- package/dist/thread/__tests__/namespace.test.d.ts +2 -0
- package/dist/thread/__tests__/namespace.test.d.ts.map +1 -0
- package/dist/thread/__tests__/namespace.test.js +131 -0
- package/dist/thread/__tests__/thread-persistence.test.d.ts +2 -0
- package/dist/thread/__tests__/thread-persistence.test.d.ts.map +1 -0
- package/dist/thread/__tests__/thread-persistence.test.js +351 -0
- package/dist/thread/__tests__/thread.test.js +49 -51
- package/dist/thread/thread.d.ts +70 -18
- package/dist/thread/thread.d.ts.map +1 -1
- package/dist/thread/thread.js +211 -73
- package/dist/thread/utils.d.ts +36 -8
- package/dist/thread/utils.d.ts.map +1 -1
- package/dist/thread/utils.js +52 -8
- package/dist/tool/__tests__/fixtures.js +1 -1
- package/dist/tool/__tests__/toolkit.test.js +15 -12
- package/dist/tool/tool.js +3 -3
- package/dist/types/kernl.d.ts +42 -0
- package/dist/types/kernl.d.ts.map +1 -0
- package/dist/types/thread.d.ts +108 -22
- package/dist/types/thread.d.ts.map +1 -1
- package/dist/types/thread.js +12 -0
- package/package.json +11 -7
- package/src/agent/__tests__/concurrency.test.ts +194 -0
- package/src/agent/__tests__/run.test.ts +441 -0
- package/src/agent/index.ts +0 -0
- package/src/agent.ts +141 -24
- package/src/api/__tests__/cursor-page.test.ts +512 -0
- package/src/api/__tests__/offset-page.test.ts +624 -0
- package/src/api/__tests__/threads.test.ts +415 -0
- package/src/api/models/index.ts +6 -0
- package/src/api/models/thread.ts +138 -0
- package/src/api/pagination/base.ts +79 -0
- package/src/api/pagination/cursor.ts +86 -0
- package/src/api/pagination/offset.ts +89 -0
- package/src/api/resources/threads/events.ts +26 -0
- package/src/api/resources/threads/index.ts +9 -0
- package/src/api/resources/threads/threads.ts +256 -0
- package/src/api/resources/threads/types.ts +143 -0
- package/src/api/resources/threads/utils.ts +104 -0
- package/src/context.ts +10 -1
- package/src/index.ts +49 -1
- package/src/internal.ts +15 -0
- package/src/kernl.ts +86 -17
- package/src/mcp/__tests__/integration.test.ts +8 -9
- package/src/mcp/__tests__/utils.test.ts +6 -6
- package/src/mcp/http.ts +9 -9
- package/src/mcp/sse.ts +7 -7
- package/src/mcp/utils.ts +6 -5
- package/src/storage/__tests__/in-memory.test.ts +534 -0
- package/src/storage/base.ts +77 -0
- package/src/storage/in-memory.ts +372 -0
- package/src/storage/index.ts +21 -0
- package/src/storage/thread.ts +141 -0
- package/src/task.ts +12 -10
- package/src/thread/__tests__/fixtures/mock-model.ts +2 -4
- package/src/thread/__tests__/integration.test.ts +111 -10
- package/src/thread/__tests__/namespace.test.ts +158 -0
- package/src/thread/__tests__/thread-persistence.test.ts +367 -0
- package/src/thread/__tests__/thread.test.ts +52 -54
- package/src/thread/thread.ts +247 -96
- package/src/thread/utils.ts +76 -13
- package/src/tool/__tests__/fixtures.ts +1 -1
- package/src/tool/__tests__/toolkit.test.ts +15 -12
- package/src/tool/tool.ts +3 -3
- package/src/types/kernl.ts +51 -0
- package/src/types/thread.ts +139 -25
- package/vitest.config.ts +1 -0
- package/dist/env.d.ts +0 -45
- package/dist/env.d.ts.map +0 -1
- package/dist/env.js +0 -31
- package/dist/error.d.ts +0 -1
- package/dist/error.d.ts.map +0 -1
- package/dist/kernel.d.ts +0 -7
- package/dist/kernel.d.ts.map +0 -1
- package/dist/kernel.js +0 -7
- package/dist/lib/serde/__tests__/codec.test.d.ts +0 -2
- package/dist/lib/serde/__tests__/codec.test.d.ts.map +0 -1
- package/dist/lib/serde/__tests__/codec.test.js +0 -75
- package/dist/lib/serde/codec.d.ts +0 -12
- package/dist/lib/serde/codec.d.ts.map +0 -1
- package/dist/lib/serde/codec.js +0 -54
- package/dist/lib/serde/thread.d.ts +0 -1
- package/dist/lib/serde/thread.d.ts.map +0 -1
- package/dist/lib/serde/thread.js +0 -172
- package/dist/lib/serde/tool.d.ts +0 -36
- package/dist/lib/serde/tool.d.ts.map +0 -1
- package/dist/lib/utils.d.ts +0 -19
- package/dist/lib/utils.d.ts.map +0 -1
- package/dist/lib/utils.js +0 -41
- package/dist/logger.d.ts +0 -36
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -43
- package/dist/mcp/__tests__/fixtures/echo-server.d.ts +0 -3
- package/dist/mcp/__tests__/fixtures/echo-server.d.ts.map +0 -1
- package/dist/mcp/__tests__/fixtures/echo-server.js +0 -92
- package/dist/mcp/__tests__/fixtures/math-server.d.ts +0 -3
- package/dist/mcp/__tests__/fixtures/math-server.d.ts.map +0 -1
- package/dist/mcp/__tests__/fixtures/math-server.js +0 -98
- package/dist/mcp/__tests__/fixtures/test-server.d.ts +0 -3
- package/dist/mcp/__tests__/fixtures/test-server.d.ts.map +0 -1
- package/dist/mcp/__tests__/fixtures/test-server.js +0 -163
- package/dist/mcp/__tests__/test-utils.d.ts +0 -17
- package/dist/mcp/__tests__/test-utils.d.ts.map +0 -1
- package/dist/mcp/__tests__/test-utils.js +0 -42
- package/dist/mcp/node.d.ts +0 -60
- package/dist/mcp/node.d.ts.map +0 -1
- package/dist/mcp/node.js +0 -297
- package/dist/model.d.ts +0 -175
- package/dist/model.d.ts.map +0 -1
- package/dist/providers/ai.d.ts +0 -1
- package/dist/providers/ai.d.ts.map +0 -1
- package/dist/providers/ai.js +0 -1
- package/dist/providers/default.d.ts +0 -16
- package/dist/providers/default.d.ts.map +0 -1
- package/dist/providers/default.js +0 -17
- package/dist/providers/registry.d.ts +0 -1
- package/dist/providers/registry.d.ts.map +0 -1
- package/dist/providers/registry.js +0 -1
- package/dist/sched/scheduler.d.ts +0 -20
- package/dist/sched/scheduler.d.ts.map +0 -1
- package/dist/sched/task.d.ts +0 -92
- package/dist/sched/task.d.ts.map +0 -1
- package/dist/sched/task.js +0 -102
- package/dist/serde/__tests__/codec.test.d.ts +0 -2
- package/dist/serde/__tests__/codec.test.d.ts.map +0 -1
- package/dist/serde/__tests__/codec.test.js +0 -75
- package/dist/serde/codec.d.ts +0 -12
- package/dist/serde/codec.d.ts.map +0 -1
- package/dist/serde/codec.js +0 -54
- package/dist/serde/json.d.ts +0 -8
- package/dist/serde/json.d.ts.map +0 -1
- package/dist/serde/json.js +0 -13
- package/dist/serde/thread.d.ts +0 -687
- package/dist/serde/thread.d.ts.map +0 -1
- package/dist/serde/thread.js +0 -158
- package/dist/serde/tool.d.ts +0 -36
- package/dist/serde/tool.d.ts.map +0 -1
- package/dist/session.d.ts +0 -1
- package/dist/session.d.ts.map +0 -1
- package/dist/session.js +0 -1
- package/dist/thread/__tests__/stream.test.d.ts +0 -2
- package/dist/thread/__tests__/stream.test.d.ts.map +0 -1
- package/dist/thread/__tests__/stream.test.js +0 -244
- package/dist/tool/mcp.d.ts +0 -75
- package/dist/tool/mcp.d.ts.map +0 -1
- package/dist/tool/mcp.js +0 -111
- package/dist/tools.d.ts +0 -362
- package/dist/tools.d.ts.map +0 -1
- package/dist/tools.js +0 -220
- package/dist/types/proto.d.ts +0 -1551
- package/dist/types/proto.d.ts.map +0 -1
- package/dist/types/proto.js +0 -531
- package/dist/usage.d.ts +0 -43
- package/dist/usage.d.ts.map +0 -1
- package/dist/usage.js +0 -61
- package/src/lib/serde/thread.ts +0 -188
- /package/dist/{error.js → agent/index.js} +0 -0
- /package/dist/{lib/serde/tool.js → api/models/index.js} +0 -0
- /package/dist/{model.js → api/models/thread.js} +0 -0
- /package/dist/{sched/scheduler.js → api/resources/threads/types.js} +0 -0
- /package/dist/{serde/tool.js → types/kernl.js} +0 -0
package/src/thread/thread.ts
CHANGED
|
@@ -1,86 +1,144 @@
|
|
|
1
1
|
import assert from "assert";
|
|
2
2
|
|
|
3
|
-
import { Kernl } from "@/kernl";
|
|
4
3
|
import { Agent } from "@/agent";
|
|
5
4
|
import { Context } from "@/context";
|
|
6
5
|
import type { Task } from "@/task";
|
|
6
|
+
import type { ResolvedAgentResponse } from "@/guardrail";
|
|
7
|
+
import type { ThreadStore } from "@/storage";
|
|
8
|
+
|
|
9
|
+
import { logger } from "@/lib/logger";
|
|
7
10
|
|
|
8
11
|
import {
|
|
9
|
-
ToolCall,
|
|
10
|
-
LanguageModel,
|
|
11
|
-
LanguageModelRequest,
|
|
12
|
-
LanguageModelItem,
|
|
13
12
|
FAILED,
|
|
14
13
|
RUNNING,
|
|
15
14
|
STOPPED,
|
|
15
|
+
message,
|
|
16
|
+
ToolCall,
|
|
17
|
+
LanguageModel,
|
|
18
|
+
LanguageModelItem,
|
|
19
|
+
LanguageModelRequest,
|
|
16
20
|
} from "@kernl-sdk/protocol";
|
|
17
21
|
import { randomID, filter } from "@kernl-sdk/shared/lib";
|
|
18
22
|
|
|
19
23
|
import type {
|
|
20
24
|
ActionSet,
|
|
21
25
|
ThreadEvent,
|
|
26
|
+
ThreadState,
|
|
22
27
|
ThreadOptions,
|
|
28
|
+
ThreadEventInner,
|
|
29
|
+
ThreadStreamEvent,
|
|
23
30
|
ThreadExecuteResult,
|
|
24
31
|
PerformActionsResult,
|
|
25
|
-
ThreadState,
|
|
26
|
-
ThreadStreamEvent,
|
|
27
32
|
} from "@/types/thread";
|
|
28
33
|
import type { AgentResponseType } from "@/types/agent";
|
|
29
|
-
import type { ResolvedAgentResponse } from "@/guardrail";
|
|
30
34
|
|
|
31
35
|
import {
|
|
36
|
+
tevent,
|
|
32
37
|
notDelta,
|
|
33
|
-
getFinalResponse,
|
|
34
38
|
getIntentions,
|
|
39
|
+
getFinalResponse,
|
|
35
40
|
parseFinalResponse,
|
|
36
41
|
} from "./utils";
|
|
37
42
|
|
|
38
43
|
/**
|
|
39
44
|
* A thread drives the execution loop for an agent.
|
|
45
|
+
*
|
|
46
|
+
* Ground principles:
|
|
47
|
+
*
|
|
48
|
+
* 1) Event log is source of truth.
|
|
49
|
+
* - Persistent storage (e.g. Postgres) is treated as an append-only per-thread log of `ThreadEvent`s:
|
|
50
|
+
* monotonic `seq`, no gaps, no updates/deletes.
|
|
51
|
+
* - `Thread.state`, `tick`, etc. are projections of that log, not an alternative source of truth.
|
|
52
|
+
*
|
|
53
|
+
* 2) Single writer per thread.
|
|
54
|
+
* - At most one executor is allowed for a given `tid` at a time.
|
|
55
|
+
* - Callers are responsible for enforcing this (e.g. locking/versioning) so two processes cannot
|
|
56
|
+
* interleave or race on `seq` or state.
|
|
57
|
+
*
|
|
58
|
+
* 3) Persist before use / observation.
|
|
59
|
+
* - Before an event can:
|
|
60
|
+
* - influence a future tick (i.e. be part of `history` fed back into the model), or
|
|
61
|
+
* - be considered “delivered” to a client,
|
|
62
|
+
* it SHOULD be durably written to storage when storage is configured.
|
|
63
|
+
*
|
|
64
|
+
* 4) Transaction boundaries match semantic steps.
|
|
65
|
+
* - The intended strategy is to buffer within a tick, then atomically persist all new events + state
|
|
66
|
+
* at the end of `tick()`.
|
|
67
|
+
* - After a crash, you only ever see whole ticks or none, never half a tick, from the store’s
|
|
68
|
+
* point of view.
|
|
69
|
+
*
|
|
70
|
+
* 5) Recovery is replay.
|
|
71
|
+
* - On restart, callers rebuild a `Thread` from the stored event log (plus optional snapshots).
|
|
72
|
+
* - Any incomplete tick or pending tool call is handled by a clear, deterministic policy at a
|
|
73
|
+
* higher layer (e.g. re-run, mark failed, or require manual intervention).
|
|
74
|
+
*
|
|
75
|
+
* On storage failures:
|
|
76
|
+
*
|
|
77
|
+
* “If storage is configured, it is authoritative” → fail hard on persist errors rather than
|
|
78
|
+
* treating persistence as best-effort.
|
|
79
|
+
*
|
|
80
|
+
* If a storage implementation is present, `persist(...)` is expected to throw on failure, and
|
|
81
|
+
* that error should bubble out of `_execute()` / `stream()` and stop the thread.
|
|
40
82
|
*/
|
|
41
83
|
export class Thread<
|
|
42
84
|
TContext = unknown,
|
|
43
85
|
TResponse extends AgentResponseType = "text",
|
|
44
86
|
> {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
readonly id: string;
|
|
87
|
+
readonly tid: string;
|
|
88
|
+
readonly namespace: string;
|
|
48
89
|
readonly agent: Agent<TContext, TResponse>;
|
|
49
90
|
readonly context: Context<TContext>;
|
|
50
91
|
readonly model: LanguageModel; /* inherited from the agent unless specified */
|
|
51
92
|
readonly parent: Task<TContext> | null; /* parent task which spawned this thread */
|
|
52
|
-
readonly
|
|
53
|
-
readonly
|
|
93
|
+
readonly createdAt: Date;
|
|
94
|
+
readonly updatedAt: Date;
|
|
95
|
+
readonly metadata: Record<string, unknown> | null;
|
|
54
96
|
// readonly stats: ThreadMetrics;
|
|
55
97
|
|
|
56
98
|
/* state */
|
|
57
|
-
_tick: number;
|
|
99
|
+
_tick: number; /* number of LLM roundtrips */
|
|
100
|
+
_seq: number; /* monotonic event sequence */
|
|
58
101
|
state: ThreadState;
|
|
59
|
-
private
|
|
102
|
+
private cpbuf: ThreadEvent[]; /* checkpoint buffer - events pending persistence */
|
|
103
|
+
private persisted: boolean; /* indicates thread was hydrated from storage */
|
|
104
|
+
private history: ThreadEvent[] /* history representing the event log for the thread */;
|
|
105
|
+
|
|
60
106
|
private abort?: AbortController;
|
|
107
|
+
private storage?: ThreadStore;
|
|
108
|
+
|
|
109
|
+
constructor(options: ThreadOptions<TContext, TResponse>) {
|
|
110
|
+
this.tid = options.tid ?? `tid_${randomID()}`;
|
|
111
|
+
this.namespace = options.namespace ?? "kernl";
|
|
112
|
+
this.agent = options.agent;
|
|
113
|
+
this.context =
|
|
114
|
+
options.context ?? new Context<TContext>(this.namespace, {} as TContext);
|
|
115
|
+
this.parent = options.task ?? null;
|
|
116
|
+
this.model = options.model ?? options.agent.model;
|
|
117
|
+
this.storage = options.storage;
|
|
118
|
+
this.createdAt = options.createdAt ?? new Date();
|
|
119
|
+
this.updatedAt = options.updatedAt ?? new Date();
|
|
120
|
+
this.metadata = options.metadata ?? null;
|
|
121
|
+
|
|
122
|
+
this._tick = options.tick ?? 0;
|
|
123
|
+
this._seq = -1;
|
|
124
|
+
this.state = options.state ?? STOPPED;
|
|
125
|
+
this.cpbuf = [];
|
|
126
|
+
this.persisted = options.persisted ?? false;
|
|
127
|
+
this.history = options.history ?? [];
|
|
128
|
+
|
|
129
|
+
// seek to latest seq (not persisted)
|
|
130
|
+
if (this.history.length > 0) {
|
|
131
|
+
this._seq = Math.max(...this.history.map((e) => e.seq));
|
|
132
|
+
}
|
|
61
133
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
options?: ThreadOptions<TContext>,
|
|
67
|
-
) {
|
|
68
|
-
this.id = `tid_${randomID()}`;
|
|
69
|
-
this.agent = agent;
|
|
70
|
-
this.context = options?.context ?? new Context<TContext>();
|
|
71
|
-
this.kernl = kernl;
|
|
72
|
-
this.parent = options?.task ?? null;
|
|
73
|
-
this.model = options?.model ?? agent.model;
|
|
74
|
-
this.mode = "blocking"; // (TODO): add streaming
|
|
75
|
-
this.input = input;
|
|
76
|
-
|
|
77
|
-
this._tick = 0;
|
|
78
|
-
this.state = STOPPED;
|
|
79
|
-
this.history = input;
|
|
134
|
+
// append initial input if provided (for new threads)
|
|
135
|
+
if (options.input && options.input.length > 0) {
|
|
136
|
+
this.append(...options.input);
|
|
137
|
+
}
|
|
80
138
|
}
|
|
81
139
|
|
|
82
140
|
/**
|
|
83
|
-
* Blocking execution
|
|
141
|
+
* Blocking execution - runs until terminal state or interruption
|
|
84
142
|
*/
|
|
85
143
|
async execute(): Promise<
|
|
86
144
|
ThreadExecuteResult<ResolvedAgentResponse<TResponse>>
|
|
@@ -89,10 +147,16 @@ export class Thread<
|
|
|
89
147
|
// just consume the stream (already in history in _execute())
|
|
90
148
|
}
|
|
91
149
|
|
|
92
|
-
//
|
|
93
|
-
const
|
|
94
|
-
|
|
150
|
+
// filter for language model items
|
|
151
|
+
const items = this.history
|
|
152
|
+
.filter((e) => e.kind !== "system")
|
|
153
|
+
.map((e) => {
|
|
154
|
+
const { tid, seq, timestamp, metadata, ...item } = e;
|
|
155
|
+
return item as LanguageModelItem;
|
|
156
|
+
});
|
|
95
157
|
|
|
158
|
+
const text = getFinalResponse(items);
|
|
159
|
+
assert(text, "_execute continues until text !== null"); // (TODO): consider preventing infinite loops here
|
|
96
160
|
const parsed = parseFinalResponse(text, this.agent.responseType);
|
|
97
161
|
|
|
98
162
|
return { response: parsed, state: this.state };
|
|
@@ -109,6 +173,10 @@ export class Thread<
|
|
|
109
173
|
this.state = RUNNING;
|
|
110
174
|
this.abort = new AbortController();
|
|
111
175
|
|
|
176
|
+
await this.checkpoint(); /* c1: persist RUNNING state + initial input */
|
|
177
|
+
|
|
178
|
+
yield { kind: "stream-start" }; // always yield start immediately
|
|
179
|
+
|
|
112
180
|
try {
|
|
113
181
|
yield* this._execute();
|
|
114
182
|
} catch (err) {
|
|
@@ -116,66 +184,68 @@ export class Thread<
|
|
|
116
184
|
} finally {
|
|
117
185
|
this.state = STOPPED;
|
|
118
186
|
this.abort = undefined;
|
|
187
|
+
await this.checkpoint(); /* c4: final checkpoint - persist STOPPED state */
|
|
119
188
|
}
|
|
120
189
|
}
|
|
121
190
|
|
|
122
191
|
/**
|
|
123
|
-
*
|
|
124
|
-
*/
|
|
125
|
-
cancel() {
|
|
126
|
-
this.abort?.abort();
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Append a new event to the thread history
|
|
131
|
-
*/
|
|
132
|
-
append(event: ThreadEvent): void {
|
|
133
|
-
this.history.push(event);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Main execution loop - always yields events, callers can propagate or discard (as in execute())
|
|
192
|
+
* Main execution loop - always yields events, callers can propagate or discard.
|
|
138
193
|
*
|
|
139
194
|
* NOTE: Streaming structured output deferred for now. Prioritizing correctness + simplicity,
|
|
140
195
|
* and unclear what use cases there would actually be for streaming a structured output (other than maybe gen UI).
|
|
141
196
|
*/
|
|
142
197
|
private async *_execute(): AsyncGenerator<ThreadStreamEvent, void> {
|
|
143
198
|
for (;;) {
|
|
199
|
+
let err = false;
|
|
200
|
+
|
|
144
201
|
if (this.abort?.signal.aborted) {
|
|
145
202
|
return;
|
|
146
203
|
}
|
|
147
204
|
|
|
148
205
|
const events = [];
|
|
149
206
|
for await (const e of this.tick()) {
|
|
207
|
+
if (e.kind === "error") {
|
|
208
|
+
err = true;
|
|
209
|
+
logger.error(e.error); // (TODO): onError callback in options
|
|
210
|
+
}
|
|
150
211
|
// we don't want deltas in the history
|
|
151
212
|
if (notDelta(e)) {
|
|
152
213
|
events.push(e);
|
|
153
|
-
this.
|
|
214
|
+
this.append(e);
|
|
154
215
|
}
|
|
155
216
|
yield e;
|
|
156
217
|
}
|
|
157
218
|
|
|
158
|
-
// if
|
|
219
|
+
// if an error event occurred → terminate
|
|
220
|
+
if (err) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// if model returns a message with no action intentions → terminal state
|
|
159
225
|
const intentions = getIntentions(events);
|
|
160
226
|
if (!intentions) {
|
|
161
227
|
const text = getFinalResponse(events);
|
|
162
228
|
if (!text) continue; // run again, policy-dependent? (how to ensure no infinite loop here?)
|
|
163
229
|
|
|
230
|
+
await this.checkpoint(); /* c2: terminal tick - no tool calls */
|
|
231
|
+
|
|
164
232
|
// await this.agent.runOutputGuardails(context, state);
|
|
165
233
|
// this.kernl.emit("thread.terminated", context, output);
|
|
166
234
|
return;
|
|
167
235
|
}
|
|
168
236
|
|
|
169
|
-
// perform
|
|
237
|
+
// perform intended actions
|
|
170
238
|
const { actions, pendingApprovals } =
|
|
171
239
|
await this.performActions(intentions);
|
|
172
240
|
|
|
173
|
-
// yield action events
|
|
241
|
+
// append + yield action events
|
|
174
242
|
for (const a of actions) {
|
|
175
|
-
this.
|
|
243
|
+
this.append(a);
|
|
176
244
|
yield a;
|
|
177
245
|
}
|
|
178
246
|
|
|
247
|
+
await this.checkpoint(); /* c3: tick complete */
|
|
248
|
+
|
|
179
249
|
if (pendingApprovals.length > 0) {
|
|
180
250
|
// publish a batch approval request containing all of them
|
|
181
251
|
//
|
|
@@ -202,22 +272,108 @@ export class Thread<
|
|
|
202
272
|
|
|
203
273
|
const req = await this.prepareModelRequest(this.history);
|
|
204
274
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
275
|
+
try {
|
|
276
|
+
if (this.model.stream) {
|
|
277
|
+
const stream = this.model.stream(req);
|
|
278
|
+
for await (const event of stream) {
|
|
279
|
+
yield event; // [text-delta, tool-call, message, reasoning, ...]
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
// fallback: blocking generate, yield events as batch
|
|
283
|
+
const res = await this.model.generate(req);
|
|
284
|
+
for (const event of res.content) {
|
|
285
|
+
yield event;
|
|
286
|
+
}
|
|
287
|
+
// (TODO): this.stats.usage.add(res.usage)
|
|
216
288
|
}
|
|
217
|
-
|
|
289
|
+
} catch (error) {
|
|
290
|
+
yield {
|
|
291
|
+
kind: "error",
|
|
292
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Persist current thread state to storage.
|
|
299
|
+
*
|
|
300
|
+
* - If storage is configured, it is authoritative - failures throw and halt execution.
|
|
301
|
+
* - No-op if storage is not configured.
|
|
302
|
+
*/
|
|
303
|
+
private async checkpoint(): Promise<void> {
|
|
304
|
+
if (!this.storage) {
|
|
305
|
+
logger.warn(
|
|
306
|
+
"thread: storage is not configured, thread will not be persisted",
|
|
307
|
+
);
|
|
308
|
+
return;
|
|
218
309
|
}
|
|
310
|
+
|
|
311
|
+
// insert thread record on first persist for new threads
|
|
312
|
+
if (!this.persisted) {
|
|
313
|
+
await this.storage.insert({
|
|
314
|
+
id: this.tid,
|
|
315
|
+
namespace: this.namespace,
|
|
316
|
+
agentId: this.agent.id,
|
|
317
|
+
parentTaskId: this.parent?.id ?? null,
|
|
318
|
+
model: `${this.model.provider}/${this.model.modelId}`,
|
|
319
|
+
context: this.context.context,
|
|
320
|
+
tick: this._tick,
|
|
321
|
+
state: this.state,
|
|
322
|
+
metadata: this.metadata,
|
|
323
|
+
});
|
|
324
|
+
this.persisted = true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// append + drain events from checkpoint buffer
|
|
328
|
+
if (this.cpbuf.length > 0) {
|
|
329
|
+
await this.storage.append(this.cpbuf);
|
|
330
|
+
this.cpbuf = [];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// update thread state
|
|
334
|
+
await this.storage.update(this.tid, {
|
|
335
|
+
state: this.state,
|
|
336
|
+
tick: this._tick,
|
|
337
|
+
context: this.context,
|
|
338
|
+
metadata: this.metadata,
|
|
339
|
+
});
|
|
219
340
|
}
|
|
220
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Append one or more items to history + enrich w/ runtime headers.
|
|
344
|
+
*
|
|
345
|
+
* Core rule:
|
|
346
|
+
*
|
|
347
|
+
* > An event becomes a ThreadEvent (and gets seq/timestamp) exactly when it is appended to history. <
|
|
348
|
+
*/
|
|
349
|
+
append(...items: ThreadEventInner[]): ThreadEvent[] {
|
|
350
|
+
const events: ThreadEvent[] = [];
|
|
351
|
+
for (const item of items) {
|
|
352
|
+
const seq = ++this._seq;
|
|
353
|
+
const e = tevent({
|
|
354
|
+
tid: this.tid,
|
|
355
|
+
seq,
|
|
356
|
+
kind: item.kind,
|
|
357
|
+
data: item,
|
|
358
|
+
});
|
|
359
|
+
this.history.push(e);
|
|
360
|
+
this.cpbuf.push(e);
|
|
361
|
+
events.push(e);
|
|
362
|
+
}
|
|
363
|
+
return events;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Cancel the running thread
|
|
368
|
+
*/
|
|
369
|
+
cancel() {
|
|
370
|
+
this.abort?.abort();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ----------------------------
|
|
374
|
+
// utils
|
|
375
|
+
// ----------------------------
|
|
376
|
+
|
|
221
377
|
/**
|
|
222
378
|
* Perform the actions returned by the model
|
|
223
379
|
*/
|
|
@@ -239,7 +395,7 @@ export class Thread<
|
|
|
239
395
|
const toolEvents = await this.executeTools(intentions.toolCalls);
|
|
240
396
|
// const mcpEvents = await this.executeMCPRequests(actions.mcpRequests);
|
|
241
397
|
|
|
242
|
-
const actions:
|
|
398
|
+
const actions: ThreadEventInner[] = [];
|
|
243
399
|
const pendingApprovals: ToolCall[] = [];
|
|
244
400
|
|
|
245
401
|
// (TODO): clean this - approval tracking should be handled differently
|
|
@@ -249,12 +405,8 @@ export class Thread<
|
|
|
249
405
|
(e.state as any) === "requires_approval" // (TODO): fix this
|
|
250
406
|
) {
|
|
251
407
|
// Find the original tool call for this pending approval
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
);
|
|
255
|
-
if (originalCall) {
|
|
256
|
-
pendingApprovals.push(originalCall);
|
|
257
|
-
}
|
|
408
|
+
const call = intentions.toolCalls.find((c) => c.callId === e.callId);
|
|
409
|
+
call && pendingApprovals.push(call);
|
|
258
410
|
} else {
|
|
259
411
|
actions.push(e);
|
|
260
412
|
}
|
|
@@ -271,7 +423,7 @@ export class Thread<
|
|
|
271
423
|
*
|
|
272
424
|
* TODO: refactor into actions system
|
|
273
425
|
*/
|
|
274
|
-
private async executeTools(calls: ToolCall[]): Promise<
|
|
426
|
+
private async executeTools(calls: ToolCall[]): Promise<ThreadEventInner[]> {
|
|
275
427
|
return await Promise.all(
|
|
276
428
|
calls.map(async (call: ToolCall) => {
|
|
277
429
|
try {
|
|
@@ -288,7 +440,7 @@ export class Thread<
|
|
|
288
440
|
|
|
289
441
|
// (TMP) - passing the approval status through the context until actions system
|
|
290
442
|
// is refined
|
|
291
|
-
const ctx = new Context(this.context.context);
|
|
443
|
+
const ctx = new Context(this.namespace, this.context.context);
|
|
292
444
|
ctx.approve(call.callId); // mark this call as approved
|
|
293
445
|
const res = await tool.invoke(ctx, call.arguments, call.callId);
|
|
294
446
|
|
|
@@ -301,7 +453,6 @@ export class Thread<
|
|
|
301
453
|
error: res.error,
|
|
302
454
|
};
|
|
303
455
|
} catch (error) {
|
|
304
|
-
// Handles both tool not found AND any execution errors
|
|
305
456
|
return {
|
|
306
457
|
kind: "tool-result" as const,
|
|
307
458
|
callId: call.callId,
|
|
@@ -325,32 +476,32 @@ export class Thread<
|
|
|
325
476
|
...this.agent.modelSettings,
|
|
326
477
|
};
|
|
327
478
|
|
|
328
|
-
//
|
|
479
|
+
// (TODO): what do we want to do with this?
|
|
329
480
|
// settings = maybeResetToolChoice(this.agent, this.state.toolUse, settings);
|
|
330
481
|
|
|
331
482
|
const system = await this.agent.instructions(this.context);
|
|
483
|
+
|
|
484
|
+
// filter for model items + strip event headers
|
|
485
|
+
const items = history
|
|
486
|
+
.filter((e) => e.kind !== "system") // system events are not sent to model
|
|
487
|
+
.map((event) => {
|
|
488
|
+
const { id, tid, seq, timestamp, metadata, ...item } = event;
|
|
489
|
+
return item as LanguageModelItem;
|
|
490
|
+
});
|
|
491
|
+
|
|
332
492
|
const input: LanguageModelItem[] = system
|
|
333
|
-
? [
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
id: randomID(),
|
|
338
|
-
role: "system",
|
|
339
|
-
content: [{ kind: "text", text: system }],
|
|
340
|
-
},
|
|
341
|
-
...history, // (TODO): filter for LanguageModelItem specifically - there may be other thread events
|
|
342
|
-
]
|
|
343
|
-
: history;
|
|
344
|
-
|
|
345
|
-
// TODO: apply custom input filters - arguably want global + agent-scoped -> apply in a middleware-like chain
|
|
493
|
+
? [message({ role: "system", text: system }), ...items]
|
|
494
|
+
: items;
|
|
495
|
+
|
|
496
|
+
// (TODO): apply custom input filters - arguably want global + agent-scoped -> apply in a middleware-like chain
|
|
346
497
|
// const filtered = await applyInputFilters(inputWithSystem, context);
|
|
347
498
|
|
|
348
499
|
const filtered = input;
|
|
349
500
|
|
|
350
501
|
// serialize action repertoire
|
|
351
|
-
const
|
|
502
|
+
const all = await this.agent.tools(this.context);
|
|
352
503
|
const enabled = await filter(
|
|
353
|
-
|
|
504
|
+
all,
|
|
354
505
|
async (tool) => await tool.isEnabled(this.context, this.agent),
|
|
355
506
|
);
|
|
356
507
|
const tools = enabled.map((tool) => tool.serialize());
|
package/src/thread/utils.ts
CHANGED
|
@@ -3,18 +3,61 @@ import { ZodType } from "zod";
|
|
|
3
3
|
import type { ResolvedAgentResponse } from "@/guardrail";
|
|
4
4
|
|
|
5
5
|
/* lib */
|
|
6
|
-
import { json } from "@kernl-sdk/shared/lib";
|
|
7
|
-
import { ToolCall } from "@kernl-sdk/protocol";
|
|
6
|
+
import { json, randomID } from "@kernl-sdk/shared/lib";
|
|
7
|
+
import { ToolCall, LanguageModelItem } from "@kernl-sdk/protocol";
|
|
8
8
|
import { ModelBehaviorError } from "@/lib/error";
|
|
9
9
|
|
|
10
10
|
/* types */
|
|
11
11
|
import type { AgentResponseType } from "@/types/agent";
|
|
12
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
ThreadEvent,
|
|
14
|
+
ThreadEventBase,
|
|
15
|
+
ThreadStreamEvent,
|
|
16
|
+
ActionSet,
|
|
17
|
+
PublicThreadEvent,
|
|
18
|
+
} from "@/types/thread";
|
|
13
19
|
|
|
14
20
|
/**
|
|
15
|
-
*
|
|
21
|
+
* Create a ThreadEvent from a LanguageModelItem with thread metadata.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* tevent({
|
|
26
|
+
* kind: "message",
|
|
27
|
+
* seq: 0,
|
|
28
|
+
* tid: "tid_123",
|
|
29
|
+
* data: message({role: "user", text: "hello"}),
|
|
30
|
+
* })
|
|
31
|
+
* // → {kind: "message", role: "user", content: [...], id: "message:msg_xyz", tid: "tid_123", seq: 0, timestamp: Date}
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function tevent(event: {
|
|
35
|
+
seq: number;
|
|
36
|
+
tid: string;
|
|
37
|
+
kind: ThreadEvent["kind"];
|
|
38
|
+
data: LanguageModelItem | null; // null for system events
|
|
39
|
+
id?: string;
|
|
40
|
+
timestamp?: Date;
|
|
41
|
+
metadata?: Record<string, unknown>;
|
|
42
|
+
}): ThreadEvent {
|
|
43
|
+
const iid = event.data ? event.data.id : undefined;
|
|
44
|
+
const defaultId = iid ? `${event.kind}:${iid}` : randomID();
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
...(event.data || {}),
|
|
48
|
+
kind: event.kind,
|
|
49
|
+
id: event.id ?? defaultId,
|
|
50
|
+
tid: event.tid,
|
|
51
|
+
seq: event.seq,
|
|
52
|
+
timestamp: event.timestamp ?? new Date(),
|
|
53
|
+
metadata: event.metadata ?? {},
|
|
54
|
+
} as ThreadEvent;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if an event is a tool call
|
|
16
59
|
*/
|
|
17
|
-
export function isActionIntention(event:
|
|
60
|
+
export function isActionIntention(event: LanguageModelItem): event is ToolCall {
|
|
18
61
|
return event.kind === "tool-call";
|
|
19
62
|
}
|
|
20
63
|
|
|
@@ -22,7 +65,7 @@ export function isActionIntention(event: ThreadEvent): event is ToolCall {
|
|
|
22
65
|
* Extract action intentions from a list of events.
|
|
23
66
|
* Returns ActionSet if there are any tool calls, null otherwise.
|
|
24
67
|
*/
|
|
25
|
-
export function getIntentions(events:
|
|
68
|
+
export function getIntentions(events: LanguageModelItem[]): ActionSet | null {
|
|
26
69
|
const toolCalls = events.filter(isActionIntention);
|
|
27
70
|
return toolCalls.length > 0 ? { toolCalls } : null;
|
|
28
71
|
}
|
|
@@ -31,7 +74,7 @@ export function getIntentions(events: ThreadEvent[]): ActionSet | null {
|
|
|
31
74
|
* Check if an event is NOT a delta/start/end event (i.e., a complete item).
|
|
32
75
|
* Returns true for complete items: Message, Reasoning, ToolCall, ToolResult
|
|
33
76
|
*/
|
|
34
|
-
export function notDelta(event: ThreadStreamEvent): event is
|
|
77
|
+
export function notDelta(event: ThreadStreamEvent): event is LanguageModelItem {
|
|
35
78
|
switch (event.kind) {
|
|
36
79
|
case "message":
|
|
37
80
|
case "reasoning":
|
|
@@ -46,16 +89,36 @@ export function notDelta(event: ThreadStreamEvent): event is ThreadEvent {
|
|
|
46
89
|
}
|
|
47
90
|
|
|
48
91
|
/**
|
|
49
|
-
*
|
|
92
|
+
* Check if an event is public/client-facing (not internal).
|
|
93
|
+
* Filters out internal system events that clients don't need.
|
|
94
|
+
*/
|
|
95
|
+
export function isPublicEvent(event: ThreadEvent): event is PublicThreadEvent {
|
|
96
|
+
switch (event.kind) {
|
|
97
|
+
case "message":
|
|
98
|
+
case "reasoning":
|
|
99
|
+
case "tool-call":
|
|
100
|
+
case "tool-result":
|
|
101
|
+
return true;
|
|
102
|
+
|
|
103
|
+
case "system":
|
|
104
|
+
return false;
|
|
105
|
+
|
|
106
|
+
default:
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Extract the final text response from a list of items.
|
|
50
113
|
* Returns null if no assistant message with text content is found.
|
|
51
114
|
*/
|
|
52
|
-
export function getFinalResponse(
|
|
115
|
+
export function getFinalResponse(items: LanguageModelItem[]): string | null {
|
|
53
116
|
// Scan backwards for the last assistant message
|
|
54
|
-
for (let i =
|
|
55
|
-
const
|
|
56
|
-
if (
|
|
117
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
118
|
+
const item = items[i];
|
|
119
|
+
if (item.kind === "message" && item.role === "assistant") {
|
|
57
120
|
// Extract text from content parts
|
|
58
|
-
for (const part of
|
|
121
|
+
for (const part of item.content) {
|
|
59
122
|
if (part.kind === "text") {
|
|
60
123
|
return part.text;
|
|
61
124
|
}
|
|
@@ -6,7 +6,7 @@ import { tool, HostedTool } from "../tool";
|
|
|
6
6
|
* Create a minimal mock context for testing
|
|
7
7
|
*/
|
|
8
8
|
export const mockContext = <T = any>(data?: T): Context<T> => {
|
|
9
|
-
return new Context<T>(data ?? ({} as T));
|
|
9
|
+
return new Context<T>("test-namespace", data ?? ({} as T));
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
/**
|