@primo-ai/core 0.1.3 → 0.1.5
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/agent.d.ts +20 -1
- package/dist/agent.js +95 -10
- package/dist/event-bus.d.ts +6 -0
- package/dist/event-bus.js +24 -0
- package/dist/gateways/gateway-chain.d.ts +4 -0
- package/dist/gateways/gateway-chain.js +6 -0
- package/dist/hook-manager.js +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/llm-invoker.d.ts +3 -3
- package/dist/llm-invoker.js +10 -9
- package/dist/loop-orchestrator.js +10 -0
- package/dist/model-factory.d.ts +4 -0
- package/dist/model-factory.js +3 -0
- package/dist/pending-permission.d.ts +16 -0
- package/dist/pending-permission.js +24 -0
- package/dist/pipeline.d.ts +3 -2
- package/dist/pipeline.js +29 -3
- package/dist/plugin-manager.js +12 -12
- package/dist/processors/evaluate-iteration.js +7 -3
- package/dist/session-storage-sqlite.d.ts +41 -0
- package/dist/session-storage-sqlite.js +239 -0
- package/dist/session-storage.d.ts +6 -0
- package/dist/session-storage.js +65 -0
- package/dist/task-manager.js +6 -2
- package/package.json +8 -4
package/dist/agent.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentConfig, CheckpointStore, PipelineStageConfig, Processor, SessionManager, Tracer } from '@primo-ai/sdk';
|
|
1
|
+
import type { AgentConfig, AgentSimpleConfig, CheckpointStore, PipelineStageConfig, Processor, SessionManager, Tracer } from '@primo-ai/sdk';
|
|
2
2
|
import { PipelineRunner } from './pipeline.js';
|
|
3
3
|
import { serialize } from './serialize.js';
|
|
4
4
|
import { ToolRegistry } from './tool-registry.js';
|
|
@@ -40,6 +40,9 @@ export declare class Agent {
|
|
|
40
40
|
private orchestrator;
|
|
41
41
|
private sessionManager?;
|
|
42
42
|
private contextBuilder;
|
|
43
|
+
private activeAbortController;
|
|
44
|
+
private lastContext?;
|
|
45
|
+
constructor(config: AgentSimpleConfig);
|
|
43
46
|
constructor(config: AgentConfig, deps?: AgentDependencies);
|
|
44
47
|
use(factory: Processor | PluginFactory): void;
|
|
45
48
|
teardown(): Promise<void>;
|
|
@@ -49,17 +52,33 @@ export declare class Agent {
|
|
|
49
52
|
get eventBus(): import('./event-bus.js').EventBus;
|
|
50
53
|
get eventSystem(): import('./event-system.js').EventSystem;
|
|
51
54
|
get state(): import('./state-machine.js').AgentState;
|
|
55
|
+
/** Subscribe to an event type. Returns an unsubscribe function. */
|
|
56
|
+
on(eventType: string, handler: (data?: unknown) => void): () => void;
|
|
57
|
+
/** Subscribe to an event type for at most one emission. */
|
|
58
|
+
once(eventType: string, handler: (data?: unknown) => void): void;
|
|
59
|
+
/** Remove a specific handler for an event type. */
|
|
60
|
+
off(eventType: string, handler: (data?: unknown) => void): void;
|
|
52
61
|
get _contextBuilder(): ContextBuilder;
|
|
53
62
|
run(input: string, signal?: globalThis.AbortSignal): Promise<AgentRunResult>;
|
|
54
63
|
resume(sessionId: string, signal?: globalThis.AbortSignal): Promise<AgentRunResult>;
|
|
55
64
|
stream(input: string, signal?: globalThis.AbortSignal): AsyncGenerator<string>;
|
|
56
65
|
streamEvents(input: string, signal?: globalThis.AbortSignal): AsyncGenerator<import('@primo-ai/sdk').StreamEvent>;
|
|
66
|
+
/** Abort a running agent. Idempotent — no-op if agent is not running. */
|
|
67
|
+
abort(): void;
|
|
68
|
+
/** Clear conversation state so the next run/stream starts a fresh session. */
|
|
69
|
+
reset(): void;
|
|
57
70
|
/** Clear the cached model so the next run re-resolves from the factory. */
|
|
58
71
|
invalidateModel(): void;
|
|
59
72
|
/** Auto-invalidate cached model when the error indicates auth failure or model-not-found. */
|
|
60
73
|
private autoInvalidateModel;
|
|
61
74
|
private getLLM;
|
|
75
|
+
private buildContext;
|
|
62
76
|
private createContext;
|
|
63
77
|
private registerTools;
|
|
64
78
|
private registerBuiltinProcessors;
|
|
65
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Create an Agent from a simple config. Convenience wrapper for single-agent usage.
|
|
82
|
+
* For advanced usage (Dynamic config, providerOptions, custom dependencies), use `new Agent(config, deps)`.
|
|
83
|
+
*/
|
|
84
|
+
export declare function createAgent(config: AgentSimpleConfig): Agent;
|
package/dist/agent.js
CHANGED
|
@@ -22,6 +22,8 @@ export class Agent {
|
|
|
22
22
|
orchestrator;
|
|
23
23
|
sessionManager;
|
|
24
24
|
contextBuilder;
|
|
25
|
+
activeAbortController = null;
|
|
26
|
+
lastContext;
|
|
25
27
|
constructor(config, deps) {
|
|
26
28
|
this.config = config;
|
|
27
29
|
this.modelFactory = deps?.modelFactory ?? new ModelFactory();
|
|
@@ -71,6 +73,18 @@ export class Agent {
|
|
|
71
73
|
get state() {
|
|
72
74
|
return this.orchestrator.state;
|
|
73
75
|
}
|
|
76
|
+
/** Subscribe to an event type. Returns an unsubscribe function. */
|
|
77
|
+
on(eventType, handler) {
|
|
78
|
+
return this.eventBus.subscribe(eventType, handler);
|
|
79
|
+
}
|
|
80
|
+
/** Subscribe to an event type for at most one emission. */
|
|
81
|
+
once(eventType, handler) {
|
|
82
|
+
this.eventBus.once(eventType, handler);
|
|
83
|
+
}
|
|
84
|
+
/** Remove a specific handler for an event type. */
|
|
85
|
+
off(eventType, handler) {
|
|
86
|
+
this.eventBus.unsubscribe(eventType, handler);
|
|
87
|
+
}
|
|
74
88
|
get _contextBuilder() {
|
|
75
89
|
return this.contextBuilder;
|
|
76
90
|
}
|
|
@@ -78,19 +92,25 @@ export class Agent {
|
|
|
78
92
|
if (signal?.aborted)
|
|
79
93
|
throw new DOMException('Agent run aborted', 'AbortError');
|
|
80
94
|
this._pluginManager.freezeHarnessInstances();
|
|
81
|
-
const context = await this.
|
|
95
|
+
const context = await this.buildContext(input);
|
|
82
96
|
const hm = this._pluginManager.hookManager;
|
|
83
97
|
// agent.start hook
|
|
84
98
|
await hm.invoke('agent.start', { sessionId: context.request.sessionId, request: context.request, agentConfig: this.config }, {});
|
|
85
99
|
const maxIter = typeof this.config.maxIterations === 'number' ? this.config.maxIterations : 10;
|
|
100
|
+
const controller = new AbortController();
|
|
101
|
+
this.activeAbortController = controller;
|
|
102
|
+
if (signal) {
|
|
103
|
+
signal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
104
|
+
}
|
|
86
105
|
try {
|
|
87
106
|
const { context: finalCtx, compatRetries } = await this.orchestrator.runLoop(context, {
|
|
88
107
|
maxIterations: maxIter,
|
|
89
|
-
signal,
|
|
108
|
+
signal: controller.signal,
|
|
90
109
|
modelString: this.config.model,
|
|
91
110
|
sessionId: context.request.sessionId,
|
|
92
111
|
autoCheckpoint: this._autoCheckpoint,
|
|
93
112
|
});
|
|
113
|
+
this.lastContext = finalCtx;
|
|
94
114
|
return {
|
|
95
115
|
response: finalCtx.iteration.response ?? '',
|
|
96
116
|
tokenUsage: finalCtx.session.totalTokenUsage ?? { input: 0, output: 0 },
|
|
@@ -103,6 +123,7 @@ export class Agent {
|
|
|
103
123
|
throw error;
|
|
104
124
|
}
|
|
105
125
|
finally {
|
|
126
|
+
this.activeAbortController = null;
|
|
106
127
|
// agent.end hook — always fires, even on error; suppress hook errors to preserve original
|
|
107
128
|
try {
|
|
108
129
|
await hm.invoke('agent.end', { sessionId: context.request.sessionId }, {});
|
|
@@ -144,21 +165,36 @@ export class Agent {
|
|
|
144
165
|
async *stream(input, signal) {
|
|
145
166
|
if (signal?.aborted)
|
|
146
167
|
throw new DOMException('Agent stream aborted', 'AbortError');
|
|
147
|
-
const context = await this.
|
|
168
|
+
const context = await this.buildContext(input);
|
|
148
169
|
const hm = this._pluginManager.hookManager;
|
|
149
170
|
// agent.start hook
|
|
150
171
|
await hm.invoke('agent.start', { sessionId: context.request.sessionId, request: context.request, agentConfig: this.config }, {});
|
|
151
172
|
const maxIter = typeof this.config.maxIterations === 'number' ? this.config.maxIterations : 10;
|
|
173
|
+
const controller = new AbortController();
|
|
174
|
+
this.activeAbortController = controller;
|
|
175
|
+
if (signal) {
|
|
176
|
+
signal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
177
|
+
}
|
|
152
178
|
try {
|
|
153
|
-
|
|
179
|
+
let finalCtx;
|
|
180
|
+
for await (const event of this.orchestrator.streamEvents(context, {
|
|
154
181
|
maxIterations: maxIter,
|
|
155
|
-
signal,
|
|
182
|
+
signal: controller.signal,
|
|
156
183
|
modelString: this.config.model,
|
|
157
184
|
sessionId: context.request.sessionId,
|
|
158
185
|
autoCheckpoint: this._autoCheckpoint,
|
|
159
|
-
})
|
|
186
|
+
})) {
|
|
187
|
+
if (event.type === 'text_delta')
|
|
188
|
+
yield event.text;
|
|
189
|
+
if (event.type === 'suspended')
|
|
190
|
+
yield ` [suspended: ${event.reason}]`;
|
|
191
|
+
if (event.type === 'complete')
|
|
192
|
+
finalCtx = event.context;
|
|
193
|
+
}
|
|
194
|
+
this.lastContext = finalCtx;
|
|
160
195
|
}
|
|
161
196
|
finally {
|
|
197
|
+
this.activeAbortController = null;
|
|
162
198
|
try {
|
|
163
199
|
await hm.invoke('agent.end', { sessionId: context.request.sessionId }, {});
|
|
164
200
|
}
|
|
@@ -168,26 +204,50 @@ export class Agent {
|
|
|
168
204
|
async *streamEvents(input, signal) {
|
|
169
205
|
if (signal?.aborted)
|
|
170
206
|
throw new DOMException('Agent stream aborted', 'AbortError');
|
|
171
|
-
const context = await this.
|
|
207
|
+
const context = await this.buildContext(input);
|
|
172
208
|
const hm = this._pluginManager.hookManager;
|
|
173
209
|
await hm.invoke('agent.start', { sessionId: context.request.sessionId, request: context.request, agentConfig: this.config }, {});
|
|
174
210
|
const maxIter = typeof this.config.maxIterations === 'number' ? this.config.maxIterations : 10;
|
|
211
|
+
const controller = new AbortController();
|
|
212
|
+
this.activeAbortController = controller;
|
|
213
|
+
if (signal) {
|
|
214
|
+
signal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
215
|
+
}
|
|
175
216
|
try {
|
|
176
|
-
|
|
217
|
+
let finalCtx;
|
|
218
|
+
for await (const event of this.orchestrator.streamEvents(context, {
|
|
177
219
|
maxIterations: maxIter,
|
|
178
|
-
signal,
|
|
220
|
+
signal: controller.signal,
|
|
179
221
|
modelString: this.config.model,
|
|
180
222
|
sessionId: context.request.sessionId,
|
|
181
223
|
autoCheckpoint: this._autoCheckpoint,
|
|
182
|
-
})
|
|
224
|
+
})) {
|
|
225
|
+
if (event.type === 'complete')
|
|
226
|
+
finalCtx = event.context;
|
|
227
|
+
yield event;
|
|
228
|
+
}
|
|
229
|
+
this.lastContext = finalCtx;
|
|
183
230
|
}
|
|
184
231
|
finally {
|
|
232
|
+
this.activeAbortController = null;
|
|
185
233
|
try {
|
|
186
234
|
await hm.invoke('agent.end', { sessionId: context.request.sessionId }, {});
|
|
187
235
|
}
|
|
188
236
|
catch { /* hook error must not mask original */ }
|
|
189
237
|
}
|
|
190
238
|
}
|
|
239
|
+
/** Abort a running agent. Idempotent — no-op if agent is not running. */
|
|
240
|
+
abort() {
|
|
241
|
+
if (!this.orchestrator.stateMachine.canTransition('cancelled'))
|
|
242
|
+
return;
|
|
243
|
+
this.orchestrator.stateMachine.transition('cancelled');
|
|
244
|
+
this.activeAbortController?.abort();
|
|
245
|
+
this.activeAbortController = null;
|
|
246
|
+
}
|
|
247
|
+
/** Clear conversation state so the next run/stream starts a fresh session. */
|
|
248
|
+
reset() {
|
|
249
|
+
this.lastContext = undefined;
|
|
250
|
+
}
|
|
191
251
|
/** Clear the cached model so the next run re-resolves from the factory. */
|
|
192
252
|
invalidateModel() {
|
|
193
253
|
this._model = null;
|
|
@@ -209,6 +269,24 @@ export class Agent {
|
|
|
209
269
|
tracer: this._tracer,
|
|
210
270
|
});
|
|
211
271
|
}
|
|
272
|
+
async buildContext(input) {
|
|
273
|
+
if (this.lastContext) {
|
|
274
|
+
return {
|
|
275
|
+
request: { input, sessionId: this.lastContext.request.sessionId },
|
|
276
|
+
agent: { config: { ...this.config }, promptFragments: [], toolDeclarations: [] },
|
|
277
|
+
iteration: { step: 0 },
|
|
278
|
+
session: {
|
|
279
|
+
messageHistory: [
|
|
280
|
+
...(this.lastContext.session.messageHistory ?? []),
|
|
281
|
+
{ role: 'user', content: input },
|
|
282
|
+
],
|
|
283
|
+
totalTokenUsage: this.lastContext.session.totalTokenUsage,
|
|
284
|
+
custom: { ...this.lastContext.session.custom },
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
return this.createContext(input);
|
|
289
|
+
}
|
|
212
290
|
async createContext(input) {
|
|
213
291
|
let sessionId = crypto.randomUUID();
|
|
214
292
|
if (this.sessionManager) {
|
|
@@ -248,6 +326,13 @@ export class Agent {
|
|
|
248
326
|
this.runner.register(processOutputProcessor);
|
|
249
327
|
}
|
|
250
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* Create an Agent from a simple config. Convenience wrapper for single-agent usage.
|
|
331
|
+
* For advanced usage (Dynamic config, providerOptions, custom dependencies), use `new Agent(config, deps)`.
|
|
332
|
+
*/
|
|
333
|
+
export function createAgent(config) {
|
|
334
|
+
return new Agent(config);
|
|
335
|
+
}
|
|
251
336
|
function isAuthOrNotFoundError(error) {
|
|
252
337
|
if (error instanceof AuthError || error instanceof ModelNotFoundError)
|
|
253
338
|
return true;
|
package/dist/event-bus.d.ts
CHANGED
|
@@ -4,4 +4,10 @@ export declare class EventBus {
|
|
|
4
4
|
constructor(onError?: ((error: unknown, eventType: string) => void) | undefined);
|
|
5
5
|
emit(eventType: string, data?: unknown): void;
|
|
6
6
|
subscribe(eventType: string, handler: (data?: unknown) => void): () => void;
|
|
7
|
+
/** Remove a specific handler for an event type. */
|
|
8
|
+
unsubscribe(eventType: string, handler: (data?: unknown) => void): void;
|
|
9
|
+
/** Register a handler that fires at most once, then auto-unsubscribes. */
|
|
10
|
+
once(eventType: string, handler: (data?: unknown) => void): void;
|
|
11
|
+
/** Promise-based once: returns a Promise that resolves on the next emission. */
|
|
12
|
+
oncePromise(eventType: string): Promise<unknown>;
|
|
7
13
|
}
|
package/dist/event-bus.js
CHANGED
|
@@ -32,4 +32,28 @@ export class EventBus {
|
|
|
32
32
|
set.add(handler);
|
|
33
33
|
return () => set.delete(handler);
|
|
34
34
|
}
|
|
35
|
+
/** Remove a specific handler for an event type. */
|
|
36
|
+
unsubscribe(eventType, handler) {
|
|
37
|
+
const set = this.handlers.get(eventType);
|
|
38
|
+
if (set) {
|
|
39
|
+
set.delete(handler);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Register a handler that fires at most once, then auto-unsubscribes. */
|
|
43
|
+
once(eventType, handler) {
|
|
44
|
+
const wrapped = (data) => {
|
|
45
|
+
handler(data);
|
|
46
|
+
this.unsubscribe(eventType, wrapped);
|
|
47
|
+
};
|
|
48
|
+
this.subscribe(eventType, wrapped);
|
|
49
|
+
}
|
|
50
|
+
/** Promise-based once: returns a Promise that resolves on the next emission. */
|
|
51
|
+
oncePromise(eventType) {
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
const unsub = this.subscribe(eventType, (data) => {
|
|
54
|
+
unsub();
|
|
55
|
+
resolve(data);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
35
59
|
}
|
package/dist/hook-manager.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { PipelineRunner, type PipelineRunnerOptions } from './pipeline.js';
|
|
2
|
-
export { Agent, type AgentDependencies, type AgentRunResult } from './agent.js';
|
|
2
|
+
export { Agent, createAgent, type AgentDependencies, type AgentRunResult } from './agent.js';
|
|
3
3
|
export { LLMInvoker, type LLMInvokerOptions, type LLMInvokeInput, type LLMInvokeResult, type LLMStreamHandle } from './llm-invoker.js';
|
|
4
4
|
export { resolveModel, registerProvider, parseModel, type ParsedModel } from './model-resolver.js';
|
|
5
5
|
export { streamWithRetry, type RetryOptions } from './retry.js';
|
|
@@ -10,6 +10,7 @@ export { StorageReplayBackend } from './storage-replay-backend.js';
|
|
|
10
10
|
export { HookManager, type HookManagerOptions } from './hook-manager.js';
|
|
11
11
|
export { ToolRegistry, type AiSdkToolSchema, type ToolRegistryOptions } from './tool-registry.js';
|
|
12
12
|
export { FilesystemSessionStorage } from './session-storage.js';
|
|
13
|
+
export { SqliteSessionStorage } from './session-storage-sqlite.js';
|
|
13
14
|
export { SessionPersistence } from './session-persistence.js';
|
|
14
15
|
export { SessionManagerImpl } from './session-manager.js';
|
|
15
16
|
export { createSubAgentTool } from './sub-agent.js';
|
|
@@ -31,3 +32,5 @@ export { TiktokenCounter } from './token-counter.js';
|
|
|
31
32
|
export type { StreamEvent } from '@primo-ai/sdk';
|
|
32
33
|
export { OpenAICompatibleGateway } from './gateways/openai-compatible-gateway.js';
|
|
33
34
|
export { AgentForgeError, RecoverableError, FatalError, AuthError, ModelNotFoundError, ToolExecutionError, type AgentErrorOptions, } from './errors.js';
|
|
35
|
+
export { PermissionManager } from './pending-permission.js';
|
|
36
|
+
export type { PendingPermission } from './pending-permission.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @primo-ai/core — Agent Loop, Processor Pipeline, Context, Tool Registry
|
|
2
2
|
export { PipelineRunner } from './pipeline.js';
|
|
3
|
-
export { Agent } from './agent.js';
|
|
3
|
+
export { Agent, createAgent } from './agent.js';
|
|
4
4
|
export { LLMInvoker } from './llm-invoker.js';
|
|
5
5
|
export { resolveModel, registerProvider, parseModel } from './model-resolver.js';
|
|
6
6
|
export { streamWithRetry } from './retry.js';
|
|
@@ -11,6 +11,7 @@ export { StorageReplayBackend } from './storage-replay-backend.js';
|
|
|
11
11
|
export { HookManager } from './hook-manager.js';
|
|
12
12
|
export { ToolRegistry } from './tool-registry.js';
|
|
13
13
|
export { FilesystemSessionStorage } from './session-storage.js';
|
|
14
|
+
export { SqliteSessionStorage } from './session-storage-sqlite.js';
|
|
14
15
|
export { SessionPersistence } from './session-persistence.js';
|
|
15
16
|
export { SessionManagerImpl } from './session-manager.js';
|
|
16
17
|
export { createSubAgentTool } from './sub-agent.js';
|
|
@@ -31,3 +32,4 @@ export { HarnessAPIImpl } from './harness.js';
|
|
|
31
32
|
export { TiktokenCounter } from './token-counter.js';
|
|
32
33
|
export { OpenAICompatibleGateway } from './gateways/openai-compatible-gateway.js';
|
|
33
34
|
export { AgentForgeError, RecoverableError, FatalError, AuthError, ModelNotFoundError, ToolExecutionError, } from './errors.js';
|
|
35
|
+
export { PermissionManager } from './pending-permission.js';
|
package/dist/llm-invoker.d.ts
CHANGED
|
@@ -17,14 +17,14 @@ export interface LLMInvokeInput {
|
|
|
17
17
|
}
|
|
18
18
|
export interface LLMInvokeResult {
|
|
19
19
|
response: string;
|
|
20
|
-
tokenUsage: TokenUsage;
|
|
20
|
+
tokenUsage: TokenUsage | null;
|
|
21
21
|
}
|
|
22
22
|
export interface LLMStreamHandle {
|
|
23
23
|
fullStream: AsyncIterable<unknown>;
|
|
24
|
-
usage: Promise<TokenUsage>;
|
|
24
|
+
usage: Promise<TokenUsage | null>;
|
|
25
25
|
reasoning: Promise<string | undefined>;
|
|
26
26
|
}
|
|
27
|
-
export declare function extractTokenUsage(usage: unknown): TokenUsage;
|
|
27
|
+
export declare function extractTokenUsage(usage: unknown): TokenUsage | null;
|
|
28
28
|
export declare class LLMInvoker {
|
|
29
29
|
private options;
|
|
30
30
|
constructor(options: LLMInvokerOptions);
|
package/dist/llm-invoker.js
CHANGED
|
@@ -3,14 +3,15 @@ import { SpanType } from '@primo-ai/sdk';
|
|
|
3
3
|
import { streamWithRetry } from './retry.js';
|
|
4
4
|
export function extractTokenUsage(usage) {
|
|
5
5
|
const u = usage;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
if (!u)
|
|
7
|
+
return null;
|
|
8
|
+
const input = typeof u?.inputTokens === 'number'
|
|
9
|
+
? u.inputTokens
|
|
10
|
+
: u?.inputTokens?.total ?? 0;
|
|
11
|
+
const output = typeof u?.outputTokens === 'number'
|
|
12
|
+
? u.outputTokens
|
|
13
|
+
: u?.outputTokens?.total ?? 0;
|
|
14
|
+
return { input, output };
|
|
14
15
|
}
|
|
15
16
|
export class LLMInvoker {
|
|
16
17
|
options;
|
|
@@ -112,7 +113,7 @@ export class LLMInvoker {
|
|
|
112
113
|
.catch((err) => {
|
|
113
114
|
endSpan();
|
|
114
115
|
this.options.eventBus?.emit('llm:usage_unavailable', { error: err instanceof Error ? err.message : String(err) });
|
|
115
|
-
return
|
|
116
|
+
return null;
|
|
116
117
|
}),
|
|
117
118
|
reasoning: Promise.resolve(result.reasoningText).catch((err) => {
|
|
118
119
|
this.options.eventBus?.emit('llm:reasoning_error', { error: err instanceof Error ? err.message : String(err) });
|
|
@@ -124,6 +124,8 @@ export class LoopOrchestrator {
|
|
|
124
124
|
yield event;
|
|
125
125
|
return;
|
|
126
126
|
}
|
|
127
|
+
if (event.type === 'error')
|
|
128
|
+
throw event.error;
|
|
127
129
|
if (event.type === 'complete')
|
|
128
130
|
loopCtx = event.context;
|
|
129
131
|
yield event;
|
|
@@ -161,6 +163,14 @@ export class LoopOrchestrator {
|
|
|
161
163
|
yield event;
|
|
162
164
|
return;
|
|
163
165
|
}
|
|
166
|
+
if (event.type === 'error') {
|
|
167
|
+
if (event.recoverable) {
|
|
168
|
+
this.eventBus?.emit('pipeline:stage_error', { stage: event.stage, error: event.error });
|
|
169
|
+
loopBreak = true;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
throw event.error;
|
|
173
|
+
}
|
|
164
174
|
if (event.type === 'complete')
|
|
165
175
|
loopCtx = event.context;
|
|
166
176
|
yield event;
|
package/dist/model-factory.d.ts
CHANGED
|
@@ -8,5 +8,9 @@ export declare class ModelFactory {
|
|
|
8
8
|
resolve(modelString: string): Promise<LanguageModel>;
|
|
9
9
|
registerGateway(gateway: ModelGateway): void;
|
|
10
10
|
registerProvider(name: string, factory: ProviderFactory): void;
|
|
11
|
+
listGateways(): Array<{
|
|
12
|
+
name: string;
|
|
13
|
+
canResolve: (model: string) => boolean;
|
|
14
|
+
}>;
|
|
11
15
|
}
|
|
12
16
|
export {};
|
package/dist/model-factory.js
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface PendingPermission {
|
|
2
|
+
permissionId: string;
|
|
3
|
+
sessionId: string;
|
|
4
|
+
toolName: string;
|
|
5
|
+
args: Record<string, unknown>;
|
|
6
|
+
reason: string;
|
|
7
|
+
createdAt: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class PermissionManager {
|
|
10
|
+
private pending;
|
|
11
|
+
awaitDecision(permission: PendingPermission): Promise<boolean>;
|
|
12
|
+
resolve(permissionId: string, approved: boolean): void;
|
|
13
|
+
list(): PendingPermission[];
|
|
14
|
+
getBySession(sessionId: string): PendingPermission[];
|
|
15
|
+
get(permissionId: string): PendingPermission | undefined;
|
|
16
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export class PermissionManager {
|
|
2
|
+
pending = new Map();
|
|
3
|
+
awaitDecision(permission) {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
this.pending.set(permission.permissionId, { resolve, permission });
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
resolve(permissionId, approved) {
|
|
9
|
+
const entry = this.pending.get(permissionId);
|
|
10
|
+
if (!entry)
|
|
11
|
+
throw new Error(`Permission not found: ${permissionId}`);
|
|
12
|
+
entry.resolve(approved);
|
|
13
|
+
this.pending.delete(permissionId);
|
|
14
|
+
}
|
|
15
|
+
list() {
|
|
16
|
+
return Array.from(this.pending.values()).map(e => e.permission);
|
|
17
|
+
}
|
|
18
|
+
getBySession(sessionId) {
|
|
19
|
+
return this.list().filter(p => p.sessionId === sessionId);
|
|
20
|
+
}
|
|
21
|
+
get(permissionId) {
|
|
22
|
+
return this.pending.get(permissionId)?.permission;
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/pipeline.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { AbortSignal, PipelineContext, Processor, StageName, StreamEvent, SuspensionSignal, Tracer } from '@primo-ai/sdk';
|
|
1
|
+
import type { AbortSignal, ErrorResult, PipelineContext, Processor, StageName, StreamEvent, SuspensionSignal, Tracer } from '@primo-ai/sdk';
|
|
2
2
|
import type { HookManager } from './hook-manager.js';
|
|
3
|
-
export type RunResult = PipelineContext | AbortSignal | SuspensionSignal;
|
|
3
|
+
export type RunResult = PipelineContext | AbortSignal | SuspensionSignal | ErrorResult;
|
|
4
4
|
export interface PipelineRunnerOptions {
|
|
5
5
|
tracer?: Tracer;
|
|
6
6
|
hookManager?: HookManager;
|
|
@@ -23,5 +23,6 @@ export declare class PipelineRunner {
|
|
|
23
23
|
private executeStage;
|
|
24
24
|
private isAbort;
|
|
25
25
|
private isSuspend;
|
|
26
|
+
private isError;
|
|
26
27
|
private consumeStream;
|
|
27
28
|
}
|
package/dist/pipeline.js
CHANGED
|
@@ -100,6 +100,17 @@ export class PipelineRunner {
|
|
|
100
100
|
rootSpan.end();
|
|
101
101
|
return stageResult;
|
|
102
102
|
}
|
|
103
|
+
if (this.isError(stageResult)) {
|
|
104
|
+
stageSpan.end();
|
|
105
|
+
if (this.hookManager) {
|
|
106
|
+
try {
|
|
107
|
+
await this.hookManager.invoke('error', { error: stageResult.error, stage: stageResult.stage }, {});
|
|
108
|
+
}
|
|
109
|
+
catch { /* hook error must not mask original */ }
|
|
110
|
+
}
|
|
111
|
+
rootSpan.end();
|
|
112
|
+
return stageResult;
|
|
113
|
+
}
|
|
103
114
|
ctx = stageResult;
|
|
104
115
|
ctx = await this.consumeStream(ctx);
|
|
105
116
|
// Fire llm.after after stream is consumed (response is now available)
|
|
@@ -155,6 +166,18 @@ export class PipelineRunner {
|
|
|
155
166
|
yield { type: 'suspended', suspensionId: stageResult.suspensionId, reason: stageResult.reason, checkpoint: stageResult.checkpoint };
|
|
156
167
|
return;
|
|
157
168
|
}
|
|
169
|
+
if (this.isError(stageResult)) {
|
|
170
|
+
stageSpan.end();
|
|
171
|
+
if (this.hookManager) {
|
|
172
|
+
try {
|
|
173
|
+
await this.hookManager.invoke('error', { error: stageResult.error, stage: stageResult.stage }, {});
|
|
174
|
+
}
|
|
175
|
+
catch { /* hook error must not mask original */ }
|
|
176
|
+
}
|
|
177
|
+
rootSpan.end();
|
|
178
|
+
yield { type: 'error', error: stageResult.error, stage: stageResult.stage, recoverable: stageResult.recoverable };
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
158
181
|
ctx = stageResult;
|
|
159
182
|
const fullStream = ctx.iteration.fullStream;
|
|
160
183
|
if (fullStream) {
|
|
@@ -197,7 +220,7 @@ export class PipelineRunner {
|
|
|
197
220
|
response: chunks.join('') || ctx.iteration.response || '',
|
|
198
221
|
pendingToolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
199
222
|
reasoningContent,
|
|
200
|
-
tokenUsage: usage ?? pendingUsage,
|
|
223
|
+
tokenUsage: usage ?? pendingUsage ?? undefined,
|
|
201
224
|
fullStream: undefined,
|
|
202
225
|
usagePromise: undefined,
|
|
203
226
|
reasoningPromise: undefined,
|
|
@@ -242,7 +265,7 @@ export class PipelineRunner {
|
|
|
242
265
|
iteration: { ...currentCtx.iteration, span: stageSpan },
|
|
243
266
|
});
|
|
244
267
|
const result = await processor.execute(ctxWithSpan);
|
|
245
|
-
if ('type' in result && (result.type === 'abort' || result.type === 'suspend')) {
|
|
268
|
+
if ('type' in result && (result.type === 'abort' || result.type === 'suspend' || result.type === 'error')) {
|
|
246
269
|
return result;
|
|
247
270
|
}
|
|
248
271
|
currentCtx = deepFreeze({ ...result });
|
|
@@ -259,6 +282,9 @@ export class PipelineRunner {
|
|
|
259
282
|
isSuspend(result) {
|
|
260
283
|
return 'type' in result && result.type === 'suspend';
|
|
261
284
|
}
|
|
285
|
+
isError(result) {
|
|
286
|
+
return 'type' in result && result.type === 'error';
|
|
287
|
+
}
|
|
262
288
|
async consumeStream(ctx) {
|
|
263
289
|
const fullStream = ctx.iteration.fullStream;
|
|
264
290
|
if (!fullStream)
|
|
@@ -275,7 +301,7 @@ export class PipelineRunner {
|
|
|
275
301
|
response: result.chunks.join('') || ctx.iteration.response,
|
|
276
302
|
pendingToolCalls: result.toolCalls.length > 0 ? result.toolCalls : undefined,
|
|
277
303
|
reasoningContent,
|
|
278
|
-
tokenUsage: result.usage ?? pendingUsage,
|
|
304
|
+
tokenUsage: result.usage ?? pendingUsage ?? undefined,
|
|
279
305
|
fullStream: undefined,
|
|
280
306
|
usagePromise: undefined,
|
|
281
307
|
reasoningPromise: undefined,
|
package/dist/plugin-manager.js
CHANGED
|
@@ -52,10 +52,9 @@ export class PluginManager {
|
|
|
52
52
|
this.initializePlugin(factory);
|
|
53
53
|
}
|
|
54
54
|
catch (err) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
});
|
|
55
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
56
|
+
this.eventBus.emit('plugin:load_error', { source: filePath, error });
|
|
57
|
+
this.errors.push({ source: filePath, error });
|
|
59
58
|
}
|
|
60
59
|
}
|
|
61
60
|
initializePlugin(factory) {
|
|
@@ -97,12 +96,14 @@ export class PluginManager {
|
|
|
97
96
|
this.resourceInstances.set(resource.id, instance);
|
|
98
97
|
}
|
|
99
98
|
catch (err) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
});
|
|
99
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
100
|
+
this.eventBus.emit('plugin:resource_init_error', { source: `resource:${resource.id}`, error });
|
|
101
|
+
this.errors.push({ source: `resource:${resource.id}`, error });
|
|
104
102
|
}
|
|
105
103
|
}
|
|
104
|
+
if (this.errors.length > 0) {
|
|
105
|
+
throw new AggregateError(this.errors.map(e => e.error), `Plugin initialization failed for ${this.errors.length} resource(s): ${this.errors.map(e => e.source).join(', ')}`);
|
|
106
|
+
}
|
|
106
107
|
}
|
|
107
108
|
_shutdown = false;
|
|
108
109
|
async shutdown() {
|
|
@@ -116,10 +117,9 @@ export class PluginManager {
|
|
|
116
117
|
await resource.stop(instance);
|
|
117
118
|
}
|
|
118
119
|
catch (err) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
});
|
|
120
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
121
|
+
this.eventBus.emit('plugin:shutdown_error', { source: `resource:${resource.id}`, error });
|
|
122
|
+
this.errors.push({ source: `resource:${resource.id}`, error });
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
this.resourceInstances.clear();
|
|
@@ -24,10 +24,14 @@ export function createEvaluateIterationProcessor(deps) {
|
|
|
24
24
|
stage: 'evaluateIteration',
|
|
25
25
|
execute: async (ctx) => {
|
|
26
26
|
const prevTotal = ctx.session.totalTokenUsage ?? { input: 0, output: 0 };
|
|
27
|
-
const iterUsage = ctx.iteration.tokenUsage
|
|
27
|
+
const iterUsage = ctx.iteration.tokenUsage;
|
|
28
|
+
if (!iterUsage) {
|
|
29
|
+
eventBus?.emit('token:usage_unavailable', { step: ctx.iteration.step });
|
|
30
|
+
}
|
|
31
|
+
const safeUsage = iterUsage ?? { input: 0, output: 0 };
|
|
28
32
|
const totalTokenUsage = {
|
|
29
|
-
input: prevTotal.input +
|
|
30
|
-
output: prevTotal.output +
|
|
33
|
+
input: prevTotal.input + safeUsage.input,
|
|
34
|
+
output: prevTotal.output + safeUsage.output,
|
|
31
35
|
};
|
|
32
36
|
const totalTokens = totalTokenUsage.input + totalTokenUsage.output;
|
|
33
37
|
const requiredTools = ctx.agent.config.requiredTools;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { SessionEvent, SessionRecord, SessionStorage, SessionStatus, Message } from '@primo-ai/sdk';
|
|
2
|
+
/**
|
|
3
|
+
* SqliteSessionStorage — SQLite-backed implementation of SessionStorage.
|
|
4
|
+
*
|
|
5
|
+
* Uses `better-sqlite3` as an **optional** dependency. The module is loaded
|
|
6
|
+
* dynamically so projects that only need FilesystemSessionStorage never pay
|
|
7
|
+
* the cost (or install penalty) of the native binding.
|
|
8
|
+
*
|
|
9
|
+
* Pass `:memory:` as dbPath for an in-memory database (useful for tests).
|
|
10
|
+
*/
|
|
11
|
+
export declare class SqliteSessionStorage implements SessionStorage {
|
|
12
|
+
private db;
|
|
13
|
+
private stmtInsertEvent;
|
|
14
|
+
private stmtSelectEvents;
|
|
15
|
+
private stmtSelectSession;
|
|
16
|
+
private stmtUpsertSession;
|
|
17
|
+
private stmtDeleteSession;
|
|
18
|
+
private stmtSelectSessionsByStatus;
|
|
19
|
+
private stmtSelectSessionsByParent;
|
|
20
|
+
private stmtSelectAllSessions;
|
|
21
|
+
private stmtNextSeq;
|
|
22
|
+
constructor(dbPath: string);
|
|
23
|
+
private initSchema;
|
|
24
|
+
private prepareStatements;
|
|
25
|
+
append(sessionId: string, event: SessionEvent): Promise<void>;
|
|
26
|
+
read(sessionId: string): AsyncIterable<SessionEvent>;
|
|
27
|
+
list(filter?: {
|
|
28
|
+
parentSessionId?: string;
|
|
29
|
+
status?: SessionStatus;
|
|
30
|
+
}): Promise<SessionRecord[]>;
|
|
31
|
+
updateMeta(sessionId: string, meta: Partial<SessionRecord>): Promise<void>;
|
|
32
|
+
get(sessionId: string): Promise<SessionRecord | undefined>;
|
|
33
|
+
delete(sessionId: string): Promise<void>;
|
|
34
|
+
getMessages(sessionId: string, options?: {
|
|
35
|
+
limit?: number;
|
|
36
|
+
before?: string;
|
|
37
|
+
}): Promise<Message[]>;
|
|
38
|
+
/** Close the underlying database connection. */
|
|
39
|
+
close(): void;
|
|
40
|
+
private rowToRecord;
|
|
41
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SqliteSessionStorage — SQLite-backed implementation of SessionStorage.
|
|
3
|
+
*
|
|
4
|
+
* Uses `better-sqlite3` as an **optional** dependency. The module is loaded
|
|
5
|
+
* dynamically so projects that only need FilesystemSessionStorage never pay
|
|
6
|
+
* the cost (or install penalty) of the native binding.
|
|
7
|
+
*
|
|
8
|
+
* Pass `:memory:` as dbPath for an in-memory database (useful for tests).
|
|
9
|
+
*/
|
|
10
|
+
export class SqliteSessionStorage {
|
|
11
|
+
db;
|
|
12
|
+
// Prepared statements (lazily initialised after DB is set)
|
|
13
|
+
stmtInsertEvent;
|
|
14
|
+
stmtSelectEvents;
|
|
15
|
+
stmtSelectSession;
|
|
16
|
+
stmtUpsertSession;
|
|
17
|
+
stmtDeleteSession;
|
|
18
|
+
stmtSelectSessionsByStatus;
|
|
19
|
+
stmtSelectSessionsByParent;
|
|
20
|
+
stmtSelectAllSessions;
|
|
21
|
+
stmtNextSeq;
|
|
22
|
+
constructor(dbPath) {
|
|
23
|
+
// Dynamic import keeps better-sqlite3 truly optional
|
|
24
|
+
let Database;
|
|
25
|
+
try {
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
27
|
+
Database = require('better-sqlite3');
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw new Error('better-sqlite3 is not installed. Install it as an optional dependency or use FilesystemSessionStorage instead.');
|
|
31
|
+
}
|
|
32
|
+
this.db = new Database(dbPath);
|
|
33
|
+
this.initSchema();
|
|
34
|
+
this.prepareStatements();
|
|
35
|
+
}
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Schema initialisation
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
initSchema() {
|
|
40
|
+
this.db.pragma('journal_mode = WAL');
|
|
41
|
+
this.db.pragma('foreign_keys = ON');
|
|
42
|
+
this.db.exec(`
|
|
43
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
44
|
+
session_id TEXT PRIMARY KEY,
|
|
45
|
+
parent_session_id TEXT,
|
|
46
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
47
|
+
model TEXT,
|
|
48
|
+
input_tokens INTEGER DEFAULT 0,
|
|
49
|
+
output_tokens INTEGER DEFAULT 0,
|
|
50
|
+
created_at TEXT NOT NULL,
|
|
51
|
+
updated_at TEXT NOT NULL
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
55
|
+
session_id TEXT NOT NULL,
|
|
56
|
+
seq INTEGER NOT NULL,
|
|
57
|
+
timestamp TEXT NOT NULL,
|
|
58
|
+
type TEXT NOT NULL,
|
|
59
|
+
payload TEXT NOT NULL,
|
|
60
|
+
PRIMARY KEY (session_id, seq),
|
|
61
|
+
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id);
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_events_session_seq ON events(session_id, seq);
|
|
67
|
+
`);
|
|
68
|
+
}
|
|
69
|
+
prepareStatements() {
|
|
70
|
+
this.stmtInsertEvent = this.db.prepare(`INSERT INTO events (session_id, seq, timestamp, type, payload) VALUES (?, ?, ?, ?, ?)`);
|
|
71
|
+
this.stmtSelectEvents = this.db.prepare(`SELECT seq, timestamp, type, payload FROM events WHERE session_id = ? ORDER BY seq ASC`);
|
|
72
|
+
this.stmtSelectSession = this.db.prepare(`SELECT session_id, parent_session_id, status, model, input_tokens, output_tokens, created_at, updated_at FROM sessions WHERE session_id = ?`);
|
|
73
|
+
this.stmtUpsertSession = this.db.prepare(`INSERT INTO sessions (session_id, parent_session_id, status, model, input_tokens, output_tokens, created_at, updated_at)
|
|
74
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
75
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
76
|
+
parent_session_id = COALESCE(excluded.parent_session_id, sessions.parent_session_id),
|
|
77
|
+
status = COALESCE(excluded.status, sessions.status),
|
|
78
|
+
model = COALESCE(excluded.model, sessions.model),
|
|
79
|
+
input_tokens = CASE WHEN excluded.input_tokens != 0 THEN excluded.input_tokens ELSE sessions.input_tokens END,
|
|
80
|
+
output_tokens = CASE WHEN excluded.output_tokens != 0 THEN excluded.output_tokens ELSE sessions.output_tokens END,
|
|
81
|
+
updated_at = excluded.updated_at`);
|
|
82
|
+
this.stmtDeleteSession = this.db.prepare(`DELETE FROM sessions WHERE session_id = ?`);
|
|
83
|
+
this.stmtSelectSessionsByStatus = this.db.prepare(`SELECT session_id, parent_session_id, status, model, input_tokens, output_tokens, created_at, updated_at FROM sessions WHERE status = ?`);
|
|
84
|
+
this.stmtSelectSessionsByParent = this.db.prepare(`SELECT session_id, parent_session_id, status, model, input_tokens, output_tokens, created_at, updated_at FROM sessions WHERE parent_session_id = ?`);
|
|
85
|
+
this.stmtSelectAllSessions = this.db.prepare(`SELECT session_id, parent_session_id, status, model, input_tokens, output_tokens, created_at, updated_at FROM sessions`);
|
|
86
|
+
this.stmtNextSeq = this.db.prepare(`SELECT COALESCE(MAX(seq), 0) AS max_seq FROM events WHERE session_id = ?`);
|
|
87
|
+
}
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// SessionStorage interface
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
async append(sessionId, event) {
|
|
92
|
+
// Ensure session row exists
|
|
93
|
+
const now = new Date().toISOString();
|
|
94
|
+
this.stmtUpsertSession.run(sessionId, null, // parent_session_id
|
|
95
|
+
'active', null, // model
|
|
96
|
+
0, 0, now, now);
|
|
97
|
+
// Determine seq: if event has seq > 0 use it, otherwise auto-increment
|
|
98
|
+
let seq;
|
|
99
|
+
if (event.seq && event.seq > 0) {
|
|
100
|
+
seq = event.seq;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const row = this.stmtNextSeq.get(sessionId);
|
|
104
|
+
seq = (row?.max_seq ?? 0) + 1;
|
|
105
|
+
}
|
|
106
|
+
this.stmtInsertEvent.run(sessionId, seq, event.timestamp ?? now, event.type, JSON.stringify(event.payload));
|
|
107
|
+
}
|
|
108
|
+
async *read(sessionId) {
|
|
109
|
+
const rows = this.stmtSelectEvents.all(sessionId);
|
|
110
|
+
for (const row of rows) {
|
|
111
|
+
let payload;
|
|
112
|
+
try {
|
|
113
|
+
payload = JSON.parse(row.payload);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
payload = {};
|
|
117
|
+
}
|
|
118
|
+
yield {
|
|
119
|
+
seq: row.seq,
|
|
120
|
+
timestamp: row.timestamp,
|
|
121
|
+
type: row.type,
|
|
122
|
+
payload,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async list(filter) {
|
|
127
|
+
let rows;
|
|
128
|
+
if (filter?.status) {
|
|
129
|
+
rows = this.stmtSelectSessionsByStatus.all(filter.status);
|
|
130
|
+
}
|
|
131
|
+
else if (filter?.parentSessionId) {
|
|
132
|
+
rows = this.stmtSelectSessionsByParent.all(filter.parentSessionId);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
rows = this.stmtSelectAllSessions.all();
|
|
136
|
+
}
|
|
137
|
+
// If both filters are specified, we need to intersect
|
|
138
|
+
if (filter?.status && filter?.parentSessionId) {
|
|
139
|
+
// Re-fetch with both filters using a custom query
|
|
140
|
+
const stmt = this.db.prepare(`SELECT session_id, parent_session_id, status, model, input_tokens, output_tokens, created_at, updated_at
|
|
141
|
+
FROM sessions WHERE status = ? AND parent_session_id = ?`);
|
|
142
|
+
rows = stmt.all(filter.status, filter.parentSessionId);
|
|
143
|
+
}
|
|
144
|
+
return rows.map(row => this.rowToRecord(row));
|
|
145
|
+
}
|
|
146
|
+
async updateMeta(sessionId, meta) {
|
|
147
|
+
const now = new Date().toISOString();
|
|
148
|
+
const existing = await this.get(sessionId);
|
|
149
|
+
this.stmtUpsertSession.run(sessionId, meta.parentSessionId ?? existing?.parentSessionId ?? null, meta.status ?? existing?.status ?? 'active', meta.model ?? existing?.model ?? null, meta.tokenUsage?.input ?? (existing?.tokenUsage?.input ?? 0), meta.tokenUsage?.output ?? (existing?.tokenUsage?.output ?? 0), existing?.createdAt ?? now, now);
|
|
150
|
+
}
|
|
151
|
+
async get(sessionId) {
|
|
152
|
+
const row = this.stmtSelectSession.get(sessionId);
|
|
153
|
+
if (!row)
|
|
154
|
+
return undefined;
|
|
155
|
+
return this.rowToRecord(row);
|
|
156
|
+
}
|
|
157
|
+
async delete(sessionId) {
|
|
158
|
+
this.stmtDeleteSession.run(sessionId);
|
|
159
|
+
}
|
|
160
|
+
async getMessages(sessionId, options) {
|
|
161
|
+
const events = [];
|
|
162
|
+
for await (const event of this.read(sessionId)) {
|
|
163
|
+
events.push(event);
|
|
164
|
+
}
|
|
165
|
+
if (events.length === 0)
|
|
166
|
+
return [];
|
|
167
|
+
const messages = [];
|
|
168
|
+
let toolCallIdx = 0;
|
|
169
|
+
for (const event of events) {
|
|
170
|
+
const payload = event.payload;
|
|
171
|
+
if (!payload)
|
|
172
|
+
continue;
|
|
173
|
+
switch (event.type) {
|
|
174
|
+
case 'iteration:end':
|
|
175
|
+
case 'iteration.end': {
|
|
176
|
+
if (payload.response) {
|
|
177
|
+
messages.push({ role: 'assistant', content: payload.response });
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
case 'tool:after':
|
|
182
|
+
case 'tool.after': {
|
|
183
|
+
const toolName = payload.toolName;
|
|
184
|
+
const content = payload.error
|
|
185
|
+
? String(payload.error)
|
|
186
|
+
: typeof payload.result === 'string'
|
|
187
|
+
? payload.result
|
|
188
|
+
: JSON.stringify(payload.result ?? '');
|
|
189
|
+
const msg = {
|
|
190
|
+
role: 'tool',
|
|
191
|
+
content,
|
|
192
|
+
toolCallId: `restored_${toolName}_${toolCallIdx++}`,
|
|
193
|
+
toolName,
|
|
194
|
+
};
|
|
195
|
+
if (payload.error)
|
|
196
|
+
msg.error = String(payload.error);
|
|
197
|
+
if (payload.result !== undefined)
|
|
198
|
+
msg.result = payload.result;
|
|
199
|
+
messages.push(msg);
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
case 'error': {
|
|
203
|
+
messages.push({ role: 'assistant', content: `[Error] ${String(payload.error)}` });
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
default:
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// Apply pagination
|
|
211
|
+
if (options?.limit && options.limit > 0) {
|
|
212
|
+
return messages.slice(-options.limit);
|
|
213
|
+
}
|
|
214
|
+
return messages;
|
|
215
|
+
}
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// Lifecycle
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
/** Close the underlying database connection. */
|
|
220
|
+
close() {
|
|
221
|
+
this.db.close();
|
|
222
|
+
}
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Internal helpers
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
rowToRecord(row) {
|
|
227
|
+
const inputTokens = typeof row.input_tokens === 'number' ? row.input_tokens : 0;
|
|
228
|
+
const outputTokens = typeof row.output_tokens === 'number' ? row.output_tokens : 0;
|
|
229
|
+
return {
|
|
230
|
+
sessionId: row.session_id,
|
|
231
|
+
parentSessionId: row.parent_session_id ?? undefined,
|
|
232
|
+
status: row.status ?? 'active',
|
|
233
|
+
model: row.model ?? undefined,
|
|
234
|
+
tokenUsage: (inputTokens || outputTokens) ? { input: inputTokens, output: outputTokens } : undefined,
|
|
235
|
+
createdAt: row.created_at,
|
|
236
|
+
updatedAt: row.updated_at,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -15,4 +15,10 @@ export declare class FilesystemSessionStorage implements SessionStorage {
|
|
|
15
15
|
private metaPath;
|
|
16
16
|
private readMeta;
|
|
17
17
|
private defaultRecord;
|
|
18
|
+
get(sessionId: string): Promise<SessionRecord | undefined>;
|
|
19
|
+
delete(sessionId: string): Promise<void>;
|
|
20
|
+
getMessages(sessionId: string, options?: {
|
|
21
|
+
limit?: number;
|
|
22
|
+
before?: string;
|
|
23
|
+
}): Promise<import('@primo-ai/sdk').Message[]>;
|
|
18
24
|
}
|
package/dist/session-storage.js
CHANGED
|
@@ -99,4 +99,69 @@ export class FilesystemSessionStorage {
|
|
|
99
99
|
status: 'active',
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
|
+
async get(sessionId) {
|
|
103
|
+
return this.readMeta(sessionId);
|
|
104
|
+
}
|
|
105
|
+
async delete(sessionId) {
|
|
106
|
+
const dir = this.sessionDir(sessionId);
|
|
107
|
+
const { rm } = await import('node:fs/promises');
|
|
108
|
+
await rm(dir, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
async getMessages(sessionId, options) {
|
|
111
|
+
const events = [];
|
|
112
|
+
for await (const event of this.read(sessionId)) {
|
|
113
|
+
events.push(event);
|
|
114
|
+
}
|
|
115
|
+
if (events.length === 0)
|
|
116
|
+
return [];
|
|
117
|
+
// Rebuild Message[] from events — same logic as SessionManagerImpl.restore()
|
|
118
|
+
const messages = [];
|
|
119
|
+
let toolCallIdx = 0;
|
|
120
|
+
for (const event of events) {
|
|
121
|
+
const payload = event.payload;
|
|
122
|
+
if (!payload)
|
|
123
|
+
continue;
|
|
124
|
+
switch (event.type) {
|
|
125
|
+
case 'iteration:end':
|
|
126
|
+
case 'iteration.end': {
|
|
127
|
+
if (payload.response) {
|
|
128
|
+
messages.push({ role: 'assistant', content: payload.response });
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case 'tool:after':
|
|
133
|
+
case 'tool.after': {
|
|
134
|
+
const toolName = payload.toolName;
|
|
135
|
+
const content = payload.error
|
|
136
|
+
? String(payload.error)
|
|
137
|
+
: typeof payload.result === 'string'
|
|
138
|
+
? payload.result
|
|
139
|
+
: JSON.stringify(payload.result ?? '');
|
|
140
|
+
const msg = {
|
|
141
|
+
role: 'tool',
|
|
142
|
+
content,
|
|
143
|
+
toolCallId: `restored_${toolName}_${toolCallIdx++}`,
|
|
144
|
+
toolName,
|
|
145
|
+
};
|
|
146
|
+
if (payload.error)
|
|
147
|
+
msg.error = String(payload.error);
|
|
148
|
+
if (payload.result !== undefined)
|
|
149
|
+
msg.result = payload.result;
|
|
150
|
+
messages.push(msg);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case 'error': {
|
|
154
|
+
messages.push({ role: 'assistant', content: `[Error] ${String(payload.error)}` });
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
default:
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Apply pagination
|
|
162
|
+
if (options?.limit && options.limit > 0) {
|
|
163
|
+
return messages.slice(-options.limit);
|
|
164
|
+
}
|
|
165
|
+
return messages;
|
|
166
|
+
}
|
|
102
167
|
}
|
package/dist/task-manager.js
CHANGED
|
@@ -69,8 +69,12 @@ export class TaskManagerImpl {
|
|
|
69
69
|
const handle = new InternalAsyncTaskHandle(state);
|
|
70
70
|
this.handles.set(taskId, handle);
|
|
71
71
|
// Fire-and-forget the async work
|
|
72
|
-
this.executeTask(state, config, prompt).catch(() => {
|
|
73
|
-
|
|
72
|
+
this.executeTask(state, config, prompt).catch((err) => {
|
|
73
|
+
if (state.status === 'pending' || state.status === 'running') {
|
|
74
|
+
state.status = 'failed';
|
|
75
|
+
state.error = err instanceof Error ? err : new Error(String(err));
|
|
76
|
+
this.eventBus?.emit('task:error', { taskId: state.taskId, error: state.error });
|
|
77
|
+
}
|
|
74
78
|
});
|
|
75
79
|
return handle;
|
|
76
80
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@primo-ai/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -21,15 +21,19 @@
|
|
|
21
21
|
"ai": "^6.0.177",
|
|
22
22
|
"js-tiktoken": "^1.0.21",
|
|
23
23
|
"zod": "^4.4.3",
|
|
24
|
-
"@primo-ai/observability": "0.1.
|
|
25
|
-
"@primo-ai/
|
|
26
|
-
"@primo-ai/
|
|
24
|
+
"@primo-ai/observability": "0.1.5",
|
|
25
|
+
"@primo-ai/tools": "0.1.5",
|
|
26
|
+
"@primo-ai/sdk": "0.1.5"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
29
30
|
"@types/node": "^22.15.0",
|
|
30
31
|
"typescript": "^5.7.3",
|
|
31
32
|
"vitest": "^3.0.5"
|
|
32
33
|
},
|
|
34
|
+
"optionalDependencies": {
|
|
35
|
+
"better-sqlite3": "^12.9.0"
|
|
36
|
+
},
|
|
33
37
|
"scripts": {
|
|
34
38
|
"build": "tsc -p tsconfig.build.json",
|
|
35
39
|
"test": "tsc --noEmit && vitest run",
|