@primo-ai/core 0.1.4 → 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 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';
@@ -41,6 +41,8 @@ export declare class Agent {
41
41
  private sessionManager?;
42
42
  private contextBuilder;
43
43
  private activeAbortController;
44
+ private lastContext?;
45
+ constructor(config: AgentSimpleConfig);
44
46
  constructor(config: AgentConfig, deps?: AgentDependencies);
45
47
  use(factory: Processor | PluginFactory): void;
46
48
  teardown(): Promise<void>;
@@ -50,6 +52,12 @@ export declare class Agent {
50
52
  get eventBus(): import('./event-bus.js').EventBus;
51
53
  get eventSystem(): import('./event-system.js').EventSystem;
52
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;
53
61
  get _contextBuilder(): ContextBuilder;
54
62
  run(input: string, signal?: globalThis.AbortSignal): Promise<AgentRunResult>;
55
63
  resume(sessionId: string, signal?: globalThis.AbortSignal): Promise<AgentRunResult>;
@@ -57,22 +65,20 @@ export declare class Agent {
57
65
  streamEvents(input: string, signal?: globalThis.AbortSignal): AsyncGenerator<import('@primo-ai/sdk').StreamEvent>;
58
66
  /** Abort a running agent. Idempotent — no-op if agent is not running. */
59
67
  abort(): void;
60
- /**
61
- * Continue an existing session by restoring its context and running the pipeline
62
- * with a new user message.
63
- */
64
- continue(sessionId: string, message: string, signal?: globalThis.AbortSignal): Promise<AgentRunResult>;
65
- /**
66
- * Continue an existing session with streaming. Yields StreamEvents for the
67
- * continued conversation.
68
- */
69
- continueStream(sessionId: string, message: string, signal?: globalThis.AbortSignal): AsyncGenerator<import('@primo-ai/sdk').StreamEvent>;
68
+ /** Clear conversation state so the next run/stream starts a fresh session. */
69
+ reset(): void;
70
70
  /** Clear the cached model so the next run re-resolves from the factory. */
71
71
  invalidateModel(): void;
72
72
  /** Auto-invalidate cached model when the error indicates auth failure or model-not-found. */
73
73
  private autoInvalidateModel;
74
74
  private getLLM;
75
+ private buildContext;
75
76
  private createContext;
76
77
  private registerTools;
77
78
  private registerBuiltinProcessors;
78
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
@@ -23,6 +23,7 @@ export class Agent {
23
23
  sessionManager;
24
24
  contextBuilder;
25
25
  activeAbortController = null;
26
+ lastContext;
26
27
  constructor(config, deps) {
27
28
  this.config = config;
28
29
  this.modelFactory = deps?.modelFactory ?? new ModelFactory();
@@ -72,6 +73,18 @@ export class Agent {
72
73
  get state() {
73
74
  return this.orchestrator.state;
74
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
+ }
75
88
  get _contextBuilder() {
76
89
  return this.contextBuilder;
77
90
  }
@@ -79,7 +92,7 @@ export class Agent {
79
92
  if (signal?.aborted)
80
93
  throw new DOMException('Agent run aborted', 'AbortError');
81
94
  this._pluginManager.freezeHarnessInstances();
82
- const context = await this.createContext(input);
95
+ const context = await this.buildContext(input);
83
96
  const hm = this._pluginManager.hookManager;
84
97
  // agent.start hook
85
98
  await hm.invoke('agent.start', { sessionId: context.request.sessionId, request: context.request, agentConfig: this.config }, {});
@@ -97,6 +110,7 @@ export class Agent {
97
110
  sessionId: context.request.sessionId,
98
111
  autoCheckpoint: this._autoCheckpoint,
99
112
  });
113
+ this.lastContext = finalCtx;
100
114
  return {
101
115
  response: finalCtx.iteration.response ?? '',
102
116
  tokenUsage: finalCtx.session.totalTokenUsage ?? { input: 0, output: 0 },
@@ -151,7 +165,7 @@ export class Agent {
151
165
  async *stream(input, signal) {
152
166
  if (signal?.aborted)
153
167
  throw new DOMException('Agent stream aborted', 'AbortError');
154
- const context = await this.createContext(input);
168
+ const context = await this.buildContext(input);
155
169
  const hm = this._pluginManager.hookManager;
156
170
  // agent.start hook
157
171
  await hm.invoke('agent.start', { sessionId: context.request.sessionId, request: context.request, agentConfig: this.config }, {});
@@ -162,13 +176,22 @@ export class Agent {
162
176
  signal.addEventListener('abort', () => controller.abort(), { once: true });
163
177
  }
164
178
  try {
165
- yield* this.orchestrator.streamLoop(context, {
179
+ let finalCtx;
180
+ for await (const event of this.orchestrator.streamEvents(context, {
166
181
  maxIterations: maxIter,
167
182
  signal: controller.signal,
168
183
  modelString: this.config.model,
169
184
  sessionId: context.request.sessionId,
170
185
  autoCheckpoint: this._autoCheckpoint,
171
- });
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;
172
195
  }
173
196
  finally {
174
197
  this.activeAbortController = null;
@@ -181,7 +204,7 @@ export class Agent {
181
204
  async *streamEvents(input, signal) {
182
205
  if (signal?.aborted)
183
206
  throw new DOMException('Agent stream aborted', 'AbortError');
184
- const context = await this.createContext(input);
207
+ const context = await this.buildContext(input);
185
208
  const hm = this._pluginManager.hookManager;
186
209
  await hm.invoke('agent.start', { sessionId: context.request.sessionId, request: context.request, agentConfig: this.config }, {});
187
210
  const maxIter = typeof this.config.maxIterations === 'number' ? this.config.maxIterations : 10;
@@ -191,13 +214,19 @@ export class Agent {
191
214
  signal.addEventListener('abort', () => controller.abort(), { once: true });
192
215
  }
193
216
  try {
194
- yield* this.orchestrator.streamEvents(context, {
217
+ let finalCtx;
218
+ for await (const event of this.orchestrator.streamEvents(context, {
195
219
  maxIterations: maxIter,
196
220
  signal: controller.signal,
197
221
  modelString: this.config.model,
198
222
  sessionId: context.request.sessionId,
199
223
  autoCheckpoint: this._autoCheckpoint,
200
- });
224
+ })) {
225
+ if (event.type === 'complete')
226
+ finalCtx = event.context;
227
+ yield event;
228
+ }
229
+ this.lastContext = finalCtx;
201
230
  }
202
231
  finally {
203
232
  this.activeAbortController = null;
@@ -215,103 +244,9 @@ export class Agent {
215
244
  this.activeAbortController?.abort();
216
245
  this.activeAbortController = null;
217
246
  }
218
- /**
219
- * Continue an existing session by restoring its context and running the pipeline
220
- * with a new user message.
221
- */
222
- async continue(sessionId, message, signal) {
223
- if (signal?.aborted)
224
- throw new DOMException('Agent continue aborted', 'AbortError');
225
- if (!this.sessionManager)
226
- throw new Error('Session manager is required for continue()');
227
- const context = await this.sessionManager.restore(sessionId);
228
- context.request.input = message;
229
- if (context.session.messageHistory) {
230
- context.session.messageHistory.push({ role: 'user', content: message });
231
- }
232
- else {
233
- context.session.messageHistory = [{ role: 'user', content: message }];
234
- }
235
- context.iteration = { step: 0 };
236
- this._pluginManager.freezeHarnessInstances();
237
- const hm = this._pluginManager.hookManager;
238
- await hm.invoke('agent.start', { sessionId: context.request.sessionId, request: context.request, agentConfig: this.config }, {});
239
- const maxIter = typeof this.config.maxIterations === 'number' ? this.config.maxIterations : 10;
240
- const controller = new AbortController();
241
- this.activeAbortController = controller;
242
- if (signal) {
243
- signal.addEventListener('abort', () => controller.abort(), { once: true });
244
- }
245
- try {
246
- const { context: finalCtx, compatRetries } = await this.orchestrator.runLoop(context, {
247
- maxIterations: maxIter,
248
- signal: controller.signal,
249
- modelString: this.config.model,
250
- sessionId: context.request.sessionId,
251
- autoCheckpoint: this._autoCheckpoint,
252
- });
253
- return {
254
- response: finalCtx.iteration.response ?? '',
255
- tokenUsage: finalCtx.session.totalTokenUsage ?? { input: 0, output: 0 },
256
- sessionId: context.request.sessionId,
257
- compatRetries,
258
- };
259
- }
260
- catch (error) {
261
- this.autoInvalidateModel(error);
262
- throw error;
263
- }
264
- finally {
265
- this.activeAbortController = null;
266
- try {
267
- await hm.invoke('agent.end', { sessionId: context.request.sessionId }, {});
268
- }
269
- catch { /* hook error must not mask original */ }
270
- }
271
- }
272
- /**
273
- * Continue an existing session with streaming. Yields StreamEvents for the
274
- * continued conversation.
275
- */
276
- async *continueStream(sessionId, message, signal) {
277
- if (signal?.aborted)
278
- throw new DOMException('Agent continue stream aborted', 'AbortError');
279
- if (!this.sessionManager)
280
- throw new Error('Session manager is required for continueStream()');
281
- const context = await this.sessionManager.restore(sessionId);
282
- context.request.input = message;
283
- if (context.session.messageHistory) {
284
- context.session.messageHistory.push({ role: 'user', content: message });
285
- }
286
- else {
287
- context.session.messageHistory = [{ role: 'user', content: message }];
288
- }
289
- context.iteration = { step: 0 };
290
- this._pluginManager.freezeHarnessInstances();
291
- const hm = this._pluginManager.hookManager;
292
- await hm.invoke('agent.start', { sessionId: context.request.sessionId, request: context.request, agentConfig: this.config }, {});
293
- const maxIter = typeof this.config.maxIterations === 'number' ? this.config.maxIterations : 10;
294
- const controller = new AbortController();
295
- this.activeAbortController = controller;
296
- if (signal) {
297
- signal.addEventListener('abort', () => controller.abort(), { once: true });
298
- }
299
- try {
300
- yield* this.orchestrator.streamEvents(context, {
301
- maxIterations: maxIter,
302
- signal: controller.signal,
303
- modelString: this.config.model,
304
- sessionId: context.request.sessionId,
305
- autoCheckpoint: this._autoCheckpoint,
306
- });
307
- }
308
- finally {
309
- this.activeAbortController = null;
310
- try {
311
- await hm.invoke('agent.end', { sessionId: context.request.sessionId }, {});
312
- }
313
- catch { /* hook error must not mask original */ }
314
- }
247
+ /** Clear conversation state so the next run/stream starts a fresh session. */
248
+ reset() {
249
+ this.lastContext = undefined;
315
250
  }
316
251
  /** Clear the cached model so the next run re-resolves from the factory. */
317
252
  invalidateModel() {
@@ -334,6 +269,24 @@ export class Agent {
334
269
  tracer: this._tracer,
335
270
  });
336
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
+ }
337
290
  async createContext(input) {
338
291
  let sessionId = crypto.randomUUID();
339
292
  if (this.sessionManager) {
@@ -373,6 +326,13 @@ export class Agent {
373
326
  this.runner.register(processOutputProcessor);
374
327
  }
375
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
+ }
376
336
  function isAuthOrNotFoundError(error) {
377
337
  if (error instanceof AuthError || error instanceof ModelNotFoundError)
378
338
  return true;
@@ -4,5 +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
- once(eventType: string): Promise<unknown>;
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>;
8
13
  }
package/dist/event-bus.js CHANGED
@@ -32,7 +32,23 @@ export class EventBus {
32
32
  set.add(handler);
33
33
  return () => set.delete(handler);
34
34
  }
35
- once(eventType) {
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) {
36
52
  return new Promise((resolve) => {
37
53
  const unsub = this.subscribe(eventType, (data) => {
38
54
  unsub();
@@ -8,6 +8,7 @@ const HOOK_TO_EVENT = {
8
8
  'llm.after': 'llm:after',
9
9
  'tool.before': 'tool:before',
10
10
  'tool.after': 'tool:after',
11
+ 'iteration.end': 'iteration:end',
11
12
  'error': 'error',
12
13
  };
13
14
  export class HookManager {
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';
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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primo-ai/core",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -21,9 +21,9 @@
21
21
  "ai": "^6.0.177",
22
22
  "js-tiktoken": "^1.0.21",
23
23
  "zod": "^4.4.3",
24
- "@primo-ai/sdk": "0.1.4",
25
- "@primo-ai/observability": "0.1.4",
26
- "@primo-ai/tools": "0.1.4"
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
29
  "@types/better-sqlite3": "^7.6.13",