zidane 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -290,24 +290,40 @@ MCP connections are made lazily on the first `run()` call and reused across subs
290
290
 
291
291
  ## Sessions
292
292
 
293
- Sessions give an agent persistent identity, message history, and run metadata across multiple calls or restarts.
293
+ Sessions give an agent persistent identity, turn history, and run metadata across multiple calls or restarts. Each message exchange is a `SessionTurn` with its own UUID, enabling real-time multiplayer streaming.
294
+
295
+ ### SessionTurn
296
+
297
+ Every message in a session is a turn:
298
+
299
+ ```ts
300
+ interface SessionTurn {
301
+ id: string // UUID — generated by store or crypto.randomUUID()
302
+ role: 'user' | 'assistant' | 'system'
303
+ content: SessionContentBlock[] // same format used by providers
304
+ usage?: TurnUsage // token usage (assistant turns only)
305
+ createdAt: number // timestamp
306
+ }
307
+ ```
294
308
 
295
309
  ### Creating a session
296
310
 
311
+ `createSession` is async — stores can generate IDs server-side (e.g. Supabase).
312
+
297
313
  ```ts
298
314
  import { createSession, createMemoryStore } from 'zidane/session'
299
315
 
300
316
  // In-memory (default, no persistence)
301
- const session = createSession({ id: 'my-session', agentId: 'my-agent' })
317
+ const session = await createSession({ id: 'my-session', agentId: 'my-agent' })
302
318
 
303
319
  // With a store for persistence
304
320
  const store = createMemoryStore()
305
- const session = createSession({ id: 'my-session', store })
321
+ const session = await createSession({ id: 'my-session', store })
306
322
  ```
307
323
 
308
324
  ### Storage backends
309
325
 
310
- Three built-in stores are available:
326
+ Three built-in stores are available. All implement the full `SessionStore` interface including incremental operations.
311
327
 
312
328
  ```ts
313
329
  import { createMemoryStore, createSqliteStore, createRemoteStore } from 'zidane/session'
@@ -322,6 +338,38 @@ const sqliteStore = createSqliteStore({ path: './sessions.db' })
322
338
  const remoteStore = createRemoteStore({ url: 'https://api.example.com/sessions' })
323
339
  ```
324
340
 
341
+ ### SessionStore interface
342
+
343
+ ```ts
344
+ interface SessionStore {
345
+ // Optional: server-side ID generation
346
+ generateSessionId?: () => string | Promise<string>
347
+ generateTurnId?: () => string | Promise<string>
348
+
349
+ // Core CRUD
350
+ load: (sessionId: string) => Promise<SessionData | null>
351
+ save: (session: SessionData) => Promise<void>
352
+ delete: (sessionId: string) => Promise<void>
353
+ list: (filter?) => Promise<string[]>
354
+
355
+ // Incremental operations (avoids full re-save)
356
+ appendTurns: (sessionId: string, turns: SessionTurn[]) => Promise<void>
357
+ getTurns: (sessionId: string, from?: number, limit?: number) => Promise<SessionTurn[]>
358
+ updateRun: (sessionId: string, run: SessionRun) => Promise<void>
359
+ updateStatus: (sessionId: string, status: SessionStatus) => Promise<void>
360
+ }
361
+ ```
362
+
363
+ Custom ID generation lets external databases (e.g. Supabase) provide UUIDs server-side, keeping IDs in sync:
364
+
365
+ ```ts
366
+ const store = createRemoteStore({ url: '...' })
367
+ store.generateTurnId = async () => {
368
+ const { data } = await supabase.rpc('gen_random_uuid')
369
+ return data
370
+ }
371
+ ```
372
+
325
373
  ### Agent integration
326
374
 
327
375
  ```ts
@@ -335,6 +383,18 @@ await agent.run({ prompt: 'hello' })
335
383
  await session.save() // persist to store
336
384
  ```
337
385
 
386
+ Turns are persisted incrementally after each agent turn via `appendTurns` — not as a full document save. If the agent crashes mid-run, you still have turns up to the last completed turn.
387
+
388
+ ### Session status
389
+
390
+ Sessions track their status: `'idle' | 'running' | 'completed' | 'error'`. The agent updates it automatically during runs.
391
+
392
+ ```ts
393
+ session.status // 'idle'
394
+ await agent.run({ prompt: 'go' })
395
+ // idle → running → completed (or error)
396
+ ```
397
+
338
398
  ### Session hooks
339
399
 
340
400
  ```ts
@@ -347,9 +407,9 @@ agent.hooks.hook('session:end', (ctx) => {
347
407
  // ctx.status: 'completed' | 'aborted' | 'error'
348
408
  })
349
409
 
350
- agent.hooks.hook('session:messages', (ctx) => {
410
+ agent.hooks.hook('session:turns', (ctx) => {
351
411
  // ctx.sessionId, ctx.count
352
- // fired after each turn (live message sync)
412
+ // fired after each turn (incremental sync)
353
413
  })
354
414
 
355
415
  agent.hooks.hook('session:save', (ctx) => {
@@ -363,8 +423,6 @@ agent.hooks.hook('session:meta', (ctx) => {
363
423
  })
364
424
  ```
365
425
 
366
- Messages are synced to the session after every turn, not just at run start/end. If the agent crashes mid-run, you still have messages up to the last completed turn.
367
-
368
426
  ### Restoring a session
369
427
 
370
428
  ```ts
@@ -390,11 +448,12 @@ agent.hooks.hook('system:before', (ctx) => {
390
448
 
391
449
  agent.hooks.hook('turn:before', (ctx) => {
392
450
  // ctx.turn: turn number
451
+ // ctx.turnId: UUID for this turn (generated before LLM call)
393
452
  // ctx.options: StreamOptions being sent to provider
394
453
  })
395
454
 
396
455
  agent.hooks.hook('turn:after', (ctx) => {
397
- // ctx.turn, ctx.usage { input, output }
456
+ // ctx.turn, ctx.turnId, ctx.usage { input, output }
398
457
  })
399
458
 
400
459
  agent.hooks.hook('agent:done', (ctx) => {
@@ -412,10 +471,13 @@ agent.hooks.hook('agent:abort', () => {
412
471
  agent.hooks.hook('stream:text', (ctx) => {
413
472
  // ctx.delta: new text chunk
414
473
  // ctx.text: accumulated text so far
474
+ // ctx.turnId: UUID of the turn being streamed
475
+ // ctx.blockIndex: content block index within the turn
415
476
  })
416
477
 
417
478
  agent.hooks.hook('stream:end', (ctx) => {
418
479
  // ctx.text: final complete text
480
+ // ctx.turnId, ctx.blockIndex
419
481
  })
420
482
  ```
421
483
 
@@ -1,87 +1,11 @@
1
1
  import { Hookable } from 'hookable';
2
+ import { E as ExecutionContext, c as ExecutionHandle, f as SkillsConfig, d as SkillConfig } from './types-D8fzooXc.js';
2
3
  import Anthropic from '@anthropic-ai/sdk';
3
- import { M as McpServerConfig, d as TurnUsage, b as SessionMessage, C as ChildRunStats, a as AgentStats, A as AgentRunOptions, c as ToolExecutionMode } from './types-4CFQ-6Qu.js';
4
+ import { M as McpServerConfig, e as TurnUsage, b as SessionMessage, C as ChildRunStats, a as AgentStats, A as AgentRunOptions, c as SessionTurn, d as ToolExecutionMode } from './types-CLRMCak3.js';
4
5
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
5
6
  import { Provider, StreamOptions } from './providers.js';
6
7
  import { Session } from './session.js';
7
8
 
8
- /**
9
- * Execution context types.
10
- *
11
- * An execution context defines *where* and *how* an agent's tools run.
12
- * The agent loop and tools interact through this interface without knowing
13
- * whether they're running in-process, in a Docker container, or in a
14
- * remote sandbox.
15
- */
16
- interface ContextCapabilities {
17
- /** Can execute shell commands */
18
- shell: boolean;
19
- /** Can read/write files in a workspace */
20
- filesystem: boolean;
21
- /** Can make outbound network requests */
22
- network: boolean;
23
- /** Has GPU access */
24
- gpu: boolean;
25
- }
26
- /** Opaque handle to a running execution context instance */
27
- interface ExecutionHandle {
28
- id: string;
29
- type: ContextType;
30
- /** Working directory within the context */
31
- cwd: string;
32
- }
33
- interface ExecResult {
34
- stdout: string;
35
- stderr: string;
36
- exitCode: number;
37
- }
38
- interface SpawnConfig {
39
- /** Working directory (created if it doesn't exist) */
40
- cwd?: string;
41
- /** Environment variables */
42
- env?: Record<string, string>;
43
- /** Docker image (only for 'docker' context) */
44
- image?: string;
45
- /** Resource limits */
46
- limits?: {
47
- /** Memory limit in MB */
48
- memory?: number;
49
- /** CPU limit (e.g. '1.0' = 1 core) */
50
- cpu?: string;
51
- /** Timeout in seconds for the entire context lifetime */
52
- timeout?: number;
53
- };
54
- /** Sandbox provider config (only for 'sandbox' context) */
55
- sandbox?: {
56
- provider: string;
57
- apiKey?: string;
58
- [key: string]: unknown;
59
- };
60
- }
61
- type ContextType = 'process' | 'docker' | 'sandbox';
62
- interface ExecutionContext {
63
- /** Context type identifier */
64
- readonly type: ContextType;
65
- /** What this context supports */
66
- readonly capabilities: ContextCapabilities;
67
- /** Spawn a new execution environment */
68
- spawn: (config?: SpawnConfig) => Promise<ExecutionHandle>;
69
- /** Execute a shell command in the context */
70
- exec: (handle: ExecutionHandle, command: string, options?: {
71
- cwd?: string;
72
- env?: Record<string, string>;
73
- timeout?: number;
74
- }) => Promise<ExecResult>;
75
- /** Read a file from the context's filesystem */
76
- readFile: (handle: ExecutionHandle, path: string) => Promise<string>;
77
- /** Write a file to the context's filesystem */
78
- writeFile: (handle: ExecutionHandle, path: string, content: string) => Promise<void>;
79
- /** List files in a directory */
80
- listFiles: (handle: ExecutionHandle, path: string) => Promise<string[]>;
81
- /** Destroy the execution environment and clean up resources */
82
- destroy: (handle: ExecutionHandle) => Promise<void>;
83
- }
84
-
85
9
  /** Core tools available in every basic harness (without spawn) */
86
10
  declare const basicTools: {
87
11
  shell: ToolDef;
@@ -123,12 +47,19 @@ interface HarnessConfig {
123
47
  tools: Record<string, ToolDef>;
124
48
  /** MCP servers to connect and expose as tools */
125
49
  mcpServers?: McpServerConfig[];
50
+ /** Skills configuration at the harness level */
51
+ skills?: SkillsConfig;
126
52
  }
127
53
  /**
128
54
  * Define a harness with a name, optional system prompt, and tools.
129
55
  */
130
56
  declare function defineHarness(config: HarnessConfig): HarnessConfig;
131
57
  type Harness = HarnessConfig;
58
+ /**
59
+ * A harness with no tools — for pure chat mode.
60
+ * Use with `enableTools: false` or when no tool access is needed.
61
+ */
62
+ declare const noTools: HarnessConfig;
132
63
 
133
64
  /**
134
65
  * MCP (Model Context Protocol) server support.
@@ -168,18 +99,24 @@ interface AgentHooks {
168
99
  }) => void;
169
100
  'turn:before': (ctx: {
170
101
  turn: number;
102
+ turnId: string;
171
103
  options: StreamOptions;
172
104
  }) => void;
173
105
  'turn:after': (ctx: {
174
106
  turn: number;
107
+ turnId: string;
175
108
  usage: TurnUsage;
176
109
  }) => void;
177
110
  'stream:text': (ctx: {
178
111
  delta: string;
179
112
  text: string;
113
+ turnId: string;
114
+ blockIndex: number;
180
115
  }) => void;
181
116
  'stream:end': (ctx: {
182
117
  text: string;
118
+ turnId: string;
119
+ blockIndex: number;
183
120
  }) => void;
184
121
  'tool:before': (ctx: {
185
122
  name: string;
@@ -252,6 +189,16 @@ interface AgentHooks {
252
189
  input: Record<string, unknown>;
253
190
  error: Error;
254
191
  }) => void;
192
+ 'skills:resolve': (ctx: {
193
+ skills: SkillConfig[];
194
+ }) => void;
195
+ 'skills:catalog': (ctx: {
196
+ catalog: string;
197
+ skills: SkillConfig[];
198
+ }) => void;
199
+ 'skills:activate': (ctx: {
200
+ skill: SkillConfig;
201
+ }) => void;
255
202
  'agent:abort': (ctx: object) => void;
256
203
  'agent:done': (ctx: AgentStats) => void;
257
204
  'session:start': (ctx: {
@@ -264,7 +211,7 @@ interface AgentHooks {
264
211
  runId: string;
265
212
  status: 'completed' | 'aborted' | 'error';
266
213
  }) => void;
267
- 'session:messages': (ctx: {
214
+ 'session:turns': (ctx: {
268
215
  sessionId: string;
269
216
  count: number;
270
217
  }) => void;
@@ -278,16 +225,21 @@ interface AgentHooks {
278
225
  }) => void;
279
226
  }
280
227
  interface AgentOptions {
281
- harness: HarnessConfig;
228
+ /** Harness (tools + system prompt). Defaults to a no-tools harness if omitted. */
229
+ harness?: HarnessConfig;
282
230
  provider: Provider;
283
231
  /** Tool execution mode: 'sequential' (default) or 'parallel' */
284
232
  toolExecution?: ToolExecutionMode;
233
+ /** Enable tool use. When false, no tools are sent to the provider (pure chat mode). Default: true. */
234
+ enableTools?: boolean;
285
235
  /** Execution context: where tools run. Defaults to in-process. */
286
236
  execution?: ExecutionContext;
287
237
  /** MCP servers to connect and expose as tools */
288
238
  mcpServers?: McpServerConfig[];
289
- /** Session for identity, message persistence, and run tracking */
239
+ /** Session for identity, turn persistence, and run tracking */
290
240
  session?: Session;
241
+ /** Skills configuration (merged with harness-level skills, agent takes precedence) */
242
+ skills?: SkillsConfig;
291
243
  /** @internal */
292
244
  _mcpConnector?: (configs: McpServerConfig[]) => Promise<McpConnection>;
293
245
  }
@@ -302,12 +254,12 @@ interface Agent {
302
254
  /** Destroy the execution context and clean up resources */
303
255
  destroy: () => Promise<void>;
304
256
  readonly isRunning: boolean;
305
- readonly messages: SessionMessage[];
257
+ readonly turns: SessionTurn[];
306
258
  readonly execution: ExecutionContext;
307
259
  readonly handle: ExecutionHandle | null;
308
260
  readonly session: Session | null;
309
261
  meta: Record<string, unknown>;
310
262
  }
311
- declare function createAgent({ harness, provider, toolExecution, execution, mcpServers, session, _mcpConnector }: AgentOptions): Agent;
263
+ declare function createAgent({ harness: harnessOption, provider, toolExecution, enableTools, execution, mcpServers, session, skills: agentSkills, _mcpConnector }: AgentOptions): Agent;
312
264
 
313
- export { type Agent as A, type ContextCapabilities as C, type ExecutionContext as E, type Harness as H, type McpConnection as M, type SpawnConfig as S, type ToolContext as T, _default as _, type ExecResult as a, type AgentHooks as b, type AgentOptions as c, type ContextType as d, type ExecutionHandle as e, type HarnessConfig as f, type ToolDef as g, type ToolMap as h, connectMcpServers as i, createAgent as j, defineHarness as k, basicTools as l, resultToString as r };
265
+ export { type Agent as A, type Harness as H, type McpConnection as M, type ToolContext as T, _default as _, type AgentHooks as a, type AgentOptions as b, type HarnessConfig as c, type ToolDef as d, type ToolMap as e, connectMcpServers as f, createAgent as g, defineHarness as h, basicTools as i, noTools as n, resultToString as r };
@@ -21,6 +21,37 @@ function createMemoryStore() {
21
21
  ids = ids.slice(0, filter.limit);
22
22
  }
23
23
  return ids;
24
+ },
25
+ async appendTurns(sessionId, turns) {
26
+ const data = sessions.get(sessionId);
27
+ if (data) {
28
+ data.turns.push(...JSON.parse(JSON.stringify(turns)));
29
+ data.updatedAt = Date.now();
30
+ }
31
+ },
32
+ async getTurns(sessionId, from = 0, limit) {
33
+ const data = sessions.get(sessionId);
34
+ if (!data)
35
+ return [];
36
+ const sliced = data.turns.slice(from, limit !== void 0 ? from + limit : void 0);
37
+ return JSON.parse(JSON.stringify(sliced));
38
+ },
39
+ async updateRun(sessionId, run) {
40
+ const data = sessions.get(sessionId);
41
+ if (data) {
42
+ const idx = data.runs.findIndex((r) => r.id === run.id);
43
+ if (idx >= 0) {
44
+ data.runs[idx] = JSON.parse(JSON.stringify(run));
45
+ }
46
+ data.updatedAt = Date.now();
47
+ }
48
+ },
49
+ async updateStatus(sessionId, status) {
50
+ const data = sessions.get(sessionId);
51
+ if (data) {
52
+ data.status = status;
53
+ data.updatedAt = Date.now();
54
+ }
24
55
  }
25
56
  };
26
57
  }
@@ -82,6 +113,50 @@ function createRemoteStore(options) {
82
113
  }
83
114
  const body = await res.json();
84
115
  return body.ids;
116
+ },
117
+ async appendTurns(sessionId, turns) {
118
+ const res = await request(`/sessions/${encodeURIComponent(sessionId)}/turns`, {
119
+ method: "POST",
120
+ body: JSON.stringify(turns)
121
+ });
122
+ if (!res.ok) {
123
+ throw new Error(`Remote appendTurns failed: ${res.status} ${res.statusText}`);
124
+ }
125
+ },
126
+ async getTurns(sessionId, from = 0, limit) {
127
+ const params = new URLSearchParams();
128
+ if (from)
129
+ params.set("from", String(from));
130
+ if (limit !== void 0)
131
+ params.set("limit", String(limit));
132
+ const query = params.toString();
133
+ const path = `/sessions/${encodeURIComponent(sessionId)}/turns${query ? `?${query}` : ""}`;
134
+ const res = await request(path);
135
+ if (!res.ok) {
136
+ throw new Error(`Remote getTurns failed: ${res.status} ${res.statusText}`);
137
+ }
138
+ return await res.json();
139
+ },
140
+ async updateRun(sessionId, run) {
141
+ const res = await request(
142
+ `/sessions/${encodeURIComponent(sessionId)}/runs/${encodeURIComponent(run.id)}`,
143
+ {
144
+ method: "PUT",
145
+ body: JSON.stringify(run)
146
+ }
147
+ );
148
+ if (!res.ok) {
149
+ throw new Error(`Remote updateRun failed: ${res.status} ${res.statusText}`);
150
+ }
151
+ },
152
+ async updateStatus(sessionId, status) {
153
+ const res = await request(`/sessions/${encodeURIComponent(sessionId)}`, {
154
+ method: "PATCH",
155
+ body: JSON.stringify({ status })
156
+ });
157
+ if (!res.ok) {
158
+ throw new Error(`Remote updateStatus failed: ${res.status} ${res.statusText}`);
159
+ }
85
160
  }
86
161
  };
87
162
  }
@@ -113,7 +188,7 @@ function createSqliteStore(options) {
113
188
  const stmtDelete = db.prepare("DELETE FROM sessions WHERE id = ?");
114
189
  const stmtList = db.prepare("SELECT id FROM sessions ORDER BY updated_at DESC");
115
190
  const stmtListByAgent = db.prepare("SELECT id FROM sessions WHERE agent_id = ? ORDER BY updated_at DESC");
116
- return {
191
+ const store = {
117
192
  async load(sessionId) {
118
193
  const row = stmtLoad.get(sessionId);
119
194
  if (!row)
@@ -144,19 +219,61 @@ function createSqliteStore(options) {
144
219
  return ids.slice(0, filter.limit);
145
220
  }
146
221
  return ids;
222
+ },
223
+ async appendTurns(sessionId, turns) {
224
+ const data = await store.load(sessionId);
225
+ if (data) {
226
+ data.turns.push(...turns);
227
+ data.updatedAt = Date.now();
228
+ await store.save(data);
229
+ }
230
+ },
231
+ async getTurns(sessionId, from = 0, limit) {
232
+ const data = await store.load(sessionId);
233
+ if (!data)
234
+ return [];
235
+ return data.turns.slice(from, limit !== void 0 ? from + limit : void 0);
236
+ },
237
+ async updateRun(sessionId, run) {
238
+ const data = await store.load(sessionId);
239
+ if (data) {
240
+ const idx = data.runs.findIndex((r) => r.id === run.id);
241
+ if (idx >= 0) {
242
+ data.runs[idx] = run;
243
+ }
244
+ data.updatedAt = Date.now();
245
+ await store.save(data);
246
+ }
247
+ },
248
+ async updateStatus(sessionId, status) {
249
+ const data = await store.load(sessionId);
250
+ if (data) {
251
+ data.status = status;
252
+ data.updatedAt = Date.now();
253
+ await store.save(data);
254
+ }
147
255
  }
148
256
  };
257
+ return store;
149
258
  }
150
259
 
151
260
  // src/session/index.ts
152
- function createSession(options = {}) {
261
+ async function createSession(options = {}) {
153
262
  const store = options.store;
154
263
  const now = Date.now();
264
+ let sessionId = options.id;
265
+ if (!sessionId && store?.generateSessionId) {
266
+ sessionId = await store.generateSessionId();
267
+ }
268
+ if (!sessionId) {
269
+ sessionId = generateId();
270
+ }
155
271
  const data = options._data ?? {
156
- id: options.id ?? generateId(),
272
+ id: sessionId,
157
273
  agentId: options.agentId,
158
- messages: [],
274
+ turns: [],
159
275
  runs: [],
276
+ status: "idle",
160
277
  metadata: options.metadata ?? {},
161
278
  createdAt: now,
162
279
  updatedAt: now
@@ -167,15 +284,18 @@ function createSession(options = {}) {
167
284
  function findRun(runId) {
168
285
  return data.runs.find((r) => r.id === runId);
169
286
  }
170
- return {
287
+ const session = {
171
288
  get id() {
172
289
  return data.id;
173
290
  },
174
291
  get agentId() {
175
292
  return data.agentId;
176
293
  },
177
- get messages() {
178
- return data.messages;
294
+ get turns() {
295
+ return data.turns;
296
+ },
297
+ get status() {
298
+ return data.status;
179
299
  },
180
300
  get runs() {
181
301
  return data.runs;
@@ -232,14 +352,35 @@ function createSession(options = {}) {
232
352
  }
233
353
  touch();
234
354
  },
235
- pushMessages(messages) {
236
- data.messages.push(...messages);
355
+ async appendTurns(turns) {
356
+ data.turns.push(...turns);
237
357
  touch();
358
+ if (store) {
359
+ await store.appendTurns(data.id, turns);
360
+ }
238
361
  },
239
- setMessages(messages) {
240
- data.messages = messages;
362
+ setTurns(turns) {
363
+ data.turns = turns;
241
364
  touch();
242
365
  },
366
+ async updateStatus(status) {
367
+ data.status = status;
368
+ touch();
369
+ if (store) {
370
+ await store.updateStatus(data.id, status);
371
+ }
372
+ },
373
+ async updateRun(run) {
374
+ if (store) {
375
+ await store.updateRun(data.id, run);
376
+ }
377
+ },
378
+ generateTurnId() {
379
+ if (store?.generateTurnId) {
380
+ return store.generateTurnId();
381
+ }
382
+ return crypto.randomUUID();
383
+ },
243
384
  setMeta(key, value) {
244
385
  data.metadata[key] = value;
245
386
  touch();
@@ -254,6 +395,7 @@ function createSession(options = {}) {
254
395
  return JSON.parse(JSON.stringify(data));
255
396
  }
256
397
  };
398
+ return session;
257
399
  }
258
400
  async function loadSession(store, sessionId) {
259
401
  const loaded = await store.load(sessionId);