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.
@@ -2,11 +2,108 @@ import {
2
2
  connectMcpServers,
3
3
  init_mcp
4
4
  } from "./chunk-26LIQARN.js";
5
+ import {
6
+ buildCatalog,
7
+ init_catalog,
8
+ init_interpolate,
9
+ init_resolve,
10
+ interpolateShellCommands,
11
+ mergeSkillsConfig,
12
+ resolveSkills
13
+ } from "./chunk-PRNQ7DXE.js";
5
14
  import {
6
15
  __esm,
7
- __export
16
+ __export,
17
+ __toCommonJS
8
18
  } from "./chunk-PNKVD2UK.js";
9
19
 
20
+ // src/tools/list-files.ts
21
+ var listFiles;
22
+ var init_list_files = __esm({
23
+ "src/tools/list-files.ts"() {
24
+ "use strict";
25
+ listFiles = {
26
+ spec: {
27
+ name: "list_files",
28
+ description: "List files and directories at the given path (relative to project root).",
29
+ input_schema: {
30
+ type: "object",
31
+ properties: {
32
+ path: { type: "string", description: 'Relative directory path (default: ".")' }
33
+ },
34
+ required: []
35
+ }
36
+ },
37
+ async execute({ path }, ctx) {
38
+ try {
39
+ const entries = await ctx.execution.listFiles(ctx.handle, path || ".");
40
+ return entries.join("\n") || "(empty directory)";
41
+ } catch {
42
+ return `Directory not found: ${path}`;
43
+ }
44
+ }
45
+ };
46
+ }
47
+ });
48
+
49
+ // src/tools/read-file.ts
50
+ var readFile;
51
+ var init_read_file = __esm({
52
+ "src/tools/read-file.ts"() {
53
+ "use strict";
54
+ readFile = {
55
+ spec: {
56
+ name: "read_file",
57
+ description: "Read the contents of a file at the given path (relative to project root).",
58
+ input_schema: {
59
+ type: "object",
60
+ properties: {
61
+ path: { type: "string", description: "Relative file path" }
62
+ },
63
+ required: ["path"]
64
+ }
65
+ },
66
+ async execute({ path }, ctx) {
67
+ try {
68
+ return await ctx.execution.readFile(ctx.handle, path);
69
+ } catch {
70
+ return `File not found: ${path}`;
71
+ }
72
+ }
73
+ };
74
+ }
75
+ });
76
+
77
+ // src/tools/shell.ts
78
+ var shell;
79
+ var init_shell = __esm({
80
+ "src/tools/shell.ts"() {
81
+ "use strict";
82
+ shell = {
83
+ spec: {
84
+ name: "shell",
85
+ description: "Execute a shell command and return stdout+stderr. Runs in the project root.",
86
+ input_schema: {
87
+ type: "object",
88
+ properties: {
89
+ command: { type: "string", description: "The shell command to run" }
90
+ },
91
+ required: ["command"]
92
+ }
93
+ },
94
+ async execute({ command }, ctx) {
95
+ const result = await ctx.execution.exec(ctx.handle, command);
96
+ if (result.exitCode === 0) {
97
+ return result.stdout || "(no output)";
98
+ }
99
+ return `Exit code ${result.exitCode}
100
+ ${result.stdout}
101
+ ${result.stderr}`.trim();
102
+ }
103
+ };
104
+ }
105
+ });
106
+
10
107
  // src/contexts/docker.ts
11
108
  function createDockerContext(config) {
12
109
  let counter = 0;
@@ -35,16 +132,16 @@ function createDockerContext(config) {
35
132
  const docker = await getDockerode();
36
133
  const id = `docker-${++counter}`;
37
134
  const image = overrides?.image ?? defaultImage;
38
- const cwd5 = overrides?.cwd ?? defaultCwd;
135
+ const cwd = overrides?.cwd ?? defaultCwd;
39
136
  try {
40
137
  await docker.getImage(image).inspect();
41
138
  } catch {
42
- await new Promise((resolve5, reject) => {
139
+ await new Promise((resolve2, reject) => {
43
140
  docker.pull(image, (err, stream) => {
44
141
  if (err)
45
142
  return reject(err);
46
143
  docker.modem.followProgress(stream, (err2) => {
47
- err2 ? reject(err2) : resolve5();
144
+ err2 ? reject(err2) : resolve2();
48
145
  });
49
146
  });
50
147
  });
@@ -61,12 +158,12 @@ function createDockerContext(config) {
61
158
  const container = await docker.createContainer({
62
159
  Image: image,
63
160
  Cmd: ["sleep", "infinity"],
64
- WorkingDir: cwd5,
161
+ WorkingDir: cwd,
65
162
  Env: Object.entries(env).map(([k, v]) => `${k}=${v}`),
66
163
  HostConfig: hostConfig
67
164
  });
68
165
  await container.start();
69
- const handle = { id, type: "docker", cwd: cwd5 };
166
+ const handle = { id, type: "docker", cwd };
70
167
  containers.set(id, { handle, container, docker });
71
168
  return handle;
72
169
  },
@@ -84,12 +181,12 @@ function createDockerContext(config) {
84
181
  AttachStderr: true
85
182
  });
86
183
  const stream = await exec.start({ Detach: false });
87
- return new Promise((resolve5) => {
184
+ return new Promise((resolve2) => {
88
185
  let stdout = "";
89
186
  const stderr = "";
90
187
  const timeout = options?.timeout ?? defaultLimits?.timeout ?? 30;
91
188
  const timer = setTimeout(() => {
92
- resolve5({ stdout, stderr: `${stderr}
189
+ resolve2({ stdout, stderr: `${stderr}
93
190
  [timeout]`, exitCode: 124 });
94
191
  }, timeout * 1e3);
95
192
  stream.on("data", (chunk) => {
@@ -98,7 +195,7 @@ function createDockerContext(config) {
98
195
  stream.on("end", async () => {
99
196
  clearTimeout(timer);
100
197
  const inspect = await exec.inspect();
101
- resolve5({ stdout, stderr, exitCode: inspect.ExitCode ?? 0 });
198
+ resolve2({ stdout, stderr, exitCode: inspect.ExitCode ?? 0 });
102
199
  });
103
200
  });
104
201
  },
@@ -145,7 +242,7 @@ var init_docker = __esm({
145
242
  // src/contexts/process.ts
146
243
  import { exec as execCb } from "child_process";
147
244
  import { mkdir, readdir, readFile as readFile2, writeFile } from "fs/promises";
148
- import { dirname, resolve as resolve3 } from "path";
245
+ import { dirname, resolve } from "path";
149
246
  import { promisify } from "util";
150
247
  function createProcessContext(config) {
151
248
  let counter = 0;
@@ -162,17 +259,17 @@ function createProcessContext(config) {
162
259
  },
163
260
  async spawn(overrides) {
164
261
  const id = `process-${++counter}`;
165
- const cwd5 = overrides?.cwd ?? defaultCwd;
166
- await mkdir(cwd5, { recursive: true });
167
- const handle = { id, type: "process", cwd: cwd5 };
262
+ const cwd = overrides?.cwd ?? defaultCwd;
263
+ await mkdir(cwd, { recursive: true });
264
+ const handle = { id, type: "process", cwd };
168
265
  handles.set(id, handle);
169
266
  return handle;
170
267
  },
171
268
  async exec(handle, command, options) {
172
- const cwd5 = options?.cwd ? resolve3(handle.cwd, options.cwd) : handle.cwd;
269
+ const cwd = options?.cwd ? resolve(handle.cwd, options.cwd) : handle.cwd;
173
270
  try {
174
271
  const { stdout, stderr } = await execAsync(command, {
175
- cwd: cwd5,
272
+ cwd,
176
273
  env: { ...process.env, ...defaultEnv, ...options?.env },
177
274
  timeout: (options?.timeout ?? config?.limits?.timeout ?? 30) * 1e3,
178
275
  maxBuffer: 10 * 1024 * 1024
@@ -187,15 +284,15 @@ function createProcessContext(config) {
187
284
  }
188
285
  },
189
286
  async readFile(handle, path) {
190
- return readFile2(resolve3(handle.cwd, path), "utf-8");
287
+ return readFile2(resolve(handle.cwd, path), "utf-8");
191
288
  },
192
289
  async writeFile(handle, path, content) {
193
- const fullPath = resolve3(handle.cwd, path);
290
+ const fullPath = resolve(handle.cwd, path);
194
291
  await mkdir(dirname(fullPath), { recursive: true });
195
292
  await writeFile(fullPath, content, "utf-8");
196
293
  },
197
294
  async listFiles(handle, path) {
198
- return readdir(resolve3(handle.cwd, path));
295
+ return readdir(resolve(handle.cwd, path));
199
296
  },
200
297
  async destroy(handle) {
201
298
  handles.delete(handle.id);
@@ -270,6 +367,52 @@ var init_contexts = __esm({
270
367
  }
271
368
  });
272
369
 
370
+ // src/harnesses/basic.ts
371
+ function getSpawn() {
372
+ if (!_spawn) {
373
+ _spawn = (init_spawn(), __toCommonJS(spawn_exports)).spawn;
374
+ }
375
+ return _spawn;
376
+ }
377
+ var basicTools, _spawn, spawnProxy, basic_default;
378
+ var init_basic = __esm({
379
+ "src/harnesses/basic.ts"() {
380
+ "use strict";
381
+ init_harnesses();
382
+ init_tools();
383
+ basicTools = { shell, readFile, writeFile: writeFile2, listFiles };
384
+ spawnProxy = {
385
+ get spec() {
386
+ return getSpawn().spec;
387
+ },
388
+ execute(input, ctx) {
389
+ return getSpawn().execute(input, ctx);
390
+ }
391
+ };
392
+ basic_default = defineHarness({
393
+ name: "basic",
394
+ system: "You are a helpful assistant with access to shell, file reading, file writing, directory listing, and sub-agent spawning tools. Use them to accomplish tasks in the project directory.",
395
+ tools: { ...basicTools, spawn: spawnProxy }
396
+ });
397
+ }
398
+ });
399
+
400
+ // src/harnesses/index.ts
401
+ function defineHarness(config) {
402
+ return config;
403
+ }
404
+ var noTools;
405
+ var init_harnesses = __esm({
406
+ "src/harnesses/index.ts"() {
407
+ init_basic();
408
+ noTools = defineHarness({
409
+ name: "none",
410
+ system: "You are a helpful assistant.",
411
+ tools: {}
412
+ });
413
+ }
414
+ });
415
+
273
416
  // src/tools/validation.ts
274
417
  function validateToolArgs(input, schema) {
275
418
  const required = schema.required ?? [];
@@ -287,6 +430,9 @@ var init_validation = __esm({
287
430
  });
288
431
 
289
432
  // src/loop.ts
433
+ function turnsToMessages(turns) {
434
+ return turns.filter((t) => t.role !== "system").map((t) => ({ role: t.role, content: t.content }));
435
+ }
290
436
  async function runLoop(ctx) {
291
437
  let totalIn = 0;
292
438
  let totalOut = 0;
@@ -309,14 +455,26 @@ async function runLoop(ctx) {
309
455
  if (ctx.steeringQueue.length > 0) {
310
456
  const steerMsg = ctx.steeringQueue.shift();
311
457
  await ctx.hooks.callHook("steer:inject", { message: steerMsg });
312
- ctx.messages.push(ctx.provider.userMessage(steerMsg));
458
+ const steerUserMsg = ctx.provider.userMessage(steerMsg);
459
+ ctx.turns.push({
460
+ id: await ctx.generateTurnId(),
461
+ role: steerUserMsg.role,
462
+ content: steerUserMsg.content,
463
+ createdAt: Date.now()
464
+ });
313
465
  continue;
314
466
  }
315
467
  if (result.ended) {
316
468
  if (ctx.followUpQueue.length > 0) {
317
469
  const followUp = ctx.followUpQueue.shift();
318
470
  await ctx.hooks.callHook("steer:inject", { message: followUp });
319
- ctx.messages.push(ctx.provider.userMessage(followUp));
471
+ const followUpMsg = ctx.provider.userMessage(followUp);
472
+ ctx.turns.push({
473
+ id: await ctx.generateTurnId(),
474
+ role: followUpMsg.role,
475
+ content: followUpMsg.content,
476
+ createdAt: Date.now()
477
+ });
320
478
  continue;
321
479
  }
322
480
  return { totalIn, totalOut, turns: turn + 1, elapsed: Date.now() - startTime, turnUsage: turnUsages };
@@ -327,37 +485,60 @@ async function runLoop(ctx) {
327
485
  return stats;
328
486
  }
329
487
  async function executeTurn(ctx, turn) {
488
+ const turnId = await ctx.generateTurnId();
489
+ const messages = turnsToMessages(ctx.turns);
330
490
  const streamOptions = {
331
491
  model: ctx.model,
332
492
  system: ctx.system,
333
493
  tools: ctx.formattedTools,
334
- messages: ctx.messages,
494
+ messages,
335
495
  maxTokens: 16384,
336
496
  thinking: ctx.thinking,
337
497
  signal: ctx.signal
338
498
  };
339
- await ctx.hooks.callHook("context:transform", { messages: ctx.messages });
340
- await ctx.hooks.callHook("turn:before", { turn, options: streamOptions });
499
+ await ctx.hooks.callHook("context:transform", { messages });
500
+ await ctx.hooks.callHook("turn:before", { turn, turnId, options: streamOptions });
341
501
  let currentText = "";
502
+ let blockIndex = 0;
342
503
  const result = await ctx.provider.stream(
343
504
  streamOptions,
344
505
  {
345
506
  onText(delta) {
346
507
  currentText += delta;
347
- ctx.hooks.callHook("stream:text", { delta, text: currentText });
508
+ ctx.hooks.callHook("stream:text", { delta, text: currentText, turnId, blockIndex });
348
509
  }
349
510
  }
350
511
  );
351
512
  if (currentText) {
352
- await ctx.hooks.callHook("stream:end", { text: currentText });
513
+ await ctx.hooks.callHook("stream:end", { text: currentText, turnId, blockIndex });
514
+ blockIndex++;
353
515
  }
354
- await ctx.hooks.callHook("turn:after", { turn, usage: result.usage });
516
+ await ctx.hooks.callHook("turn:after", { turn, turnId, usage: result.usage });
355
517
  if (result.done) {
518
+ ctx.turns.push({
519
+ id: turnId,
520
+ role: "assistant",
521
+ content: result.assistantMessage?.content ?? [{ type: "text", text: currentText }],
522
+ usage: result.usage,
523
+ createdAt: Date.now()
524
+ });
356
525
  return { ended: true, usage: result.usage };
357
526
  }
358
- ctx.messages.push(result.assistantMessage);
527
+ ctx.turns.push({
528
+ id: turnId,
529
+ role: "assistant",
530
+ content: result.assistantMessage.content,
531
+ usage: result.usage,
532
+ createdAt: Date.now()
533
+ });
359
534
  const toolResults = ctx.toolExecution === "parallel" ? await executeToolsParallel(ctx, result.toolCalls) : await executeToolsSequential(ctx, result.toolCalls);
360
- ctx.messages.push(ctx.provider.toolResultsMessage(toolResults));
535
+ const toolResultMsg = ctx.provider.toolResultsMessage(toolResults);
536
+ ctx.turns.push({
537
+ id: await ctx.generateTurnId(),
538
+ role: toolResultMsg.role,
539
+ content: toolResultMsg.content,
540
+ createdAt: Date.now()
541
+ });
361
542
  return { ended: false, usage: result.usage };
362
543
  }
363
544
  async function executeSingleTool(ctx, call) {
@@ -412,8 +593,20 @@ async function executeToolsSequential(ctx, toolCalls) {
412
593
  for (const skipped of toolCalls.slice(toolCalls.indexOf(call))) {
413
594
  results.push({ id: skipped.id, content: "Skipped: steering message received" });
414
595
  }
415
- ctx.messages.push(ctx.provider.toolResultsMessage(results));
416
- ctx.messages.push(ctx.provider.userMessage(steerMsg));
596
+ const toolResultMsg = ctx.provider.toolResultsMessage(results);
597
+ ctx.turns.push({
598
+ id: await ctx.generateTurnId(),
599
+ role: toolResultMsg.role,
600
+ content: toolResultMsg.content,
601
+ createdAt: Date.now()
602
+ });
603
+ const steerUserMsg = ctx.provider.userMessage(steerMsg);
604
+ ctx.turns.push({
605
+ id: await ctx.generateTurnId(),
606
+ role: steerUserMsg.role,
607
+ content: steerUserMsg.content,
608
+ createdAt: Date.now()
609
+ });
417
610
  return [];
418
611
  }
419
612
  const { result } = await executeSingleTool(ctx, call);
@@ -435,8 +628,9 @@ var init_loop = __esm({
435
628
 
436
629
  // src/agent.ts
437
630
  import { createHooks } from "hookable";
438
- function createAgent({ harness, provider, toolExecution = "sequential", execution, mcpServers, session, _mcpConnector }) {
631
+ function createAgent({ harness: harnessOption, provider, toolExecution = "sequential", enableTools = true, execution, mcpServers, session, skills: agentSkills, _mcpConnector }) {
439
632
  const hooks = createHooks();
633
+ const harness = harnessOption ?? noTools;
440
634
  const executionContext = execution ?? createProcessContext();
441
635
  let abortController;
442
636
  let running = false;
@@ -447,8 +641,13 @@ function createAgent({ harness, provider, toolExecution = "sequential", executio
447
641
  const allMcpServers = [...harness.mcpServers ?? [], ...mcpServers ?? []];
448
642
  const steeringQueue = [];
449
643
  const followUpQueue = [];
450
- let conversationMessages = session?.messages ?? [];
451
- let runCounter = 0;
644
+ let conversationTurns = session?.turns.slice() ?? [];
645
+ let runCounter = session?.runs.length ?? 0;
646
+ const mergedSkillsConfig = mergeSkillsConfig(harness.skills, agentSkills);
647
+ const skillsEnabledValue = mergedSkillsConfig?.enabled;
648
+ const skillsDisabled = skillsEnabledValue === false || Array.isArray(skillsEnabledValue) && skillsEnabledValue.length === 0;
649
+ let resolvedSkills = null;
650
+ let skillsCatalog = null;
452
651
  async function run(options) {
453
652
  if (running) {
454
653
  throw new Error("Agent is already running. Use steer() or followUp() to queue messages, or waitForIdle().");
@@ -457,8 +656,10 @@ function createAgent({ harness, provider, toolExecution = "sequential", executio
457
656
  abortController = new AbortController();
458
657
  const runId = `run_${++runCounter}`;
459
658
  session?.startRun(runId, options.prompt);
460
- if (session)
659
+ if (session) {
660
+ await session.updateStatus("running");
461
661
  await hooks.callHook("session:start", { sessionId: session.id, runId, prompt: options.prompt });
662
+ }
462
663
  if (options.signal) {
463
664
  if (options.signal.aborted) {
464
665
  abortController.abort();
@@ -467,8 +668,8 @@ function createAgent({ harness, provider, toolExecution = "sequential", executio
467
668
  options.signal.addEventListener("abort", onExternalAbort, { once: true });
468
669
  }
469
670
  }
470
- idlePromise = new Promise((resolve5) => {
471
- idleResolve = resolve5;
671
+ idlePromise = new Promise((resolve2) => {
672
+ idleResolve = resolve2;
472
673
  });
473
674
  const childrenStats = [];
474
675
  const unregisterSpawnHook = hooks.hook("spawn:complete", (ctx) => {
@@ -484,10 +685,52 @@ function createAgent({ harness, provider, toolExecution = "sequential", executio
484
685
  mcpConnection = await connectMcpServers(allMcpServers, void 0, hooks);
485
686
  }
486
687
  }
688
+ if (!skillsDisabled && mergedSkillsConfig && !resolvedSkills) {
689
+ resolvedSkills = await resolveSkills(mergedSkillsConfig);
690
+ await hooks.callHook("skills:resolve", { skills: resolvedSkills });
691
+ if (executionHandle) {
692
+ for (const skill of resolvedSkills) {
693
+ if (skill.instructions.includes("!`")) {
694
+ skill.instructions = await interpolateShellCommands(
695
+ skill.instructions,
696
+ executionContext,
697
+ executionHandle
698
+ );
699
+ }
700
+ }
701
+ }
702
+ const readToolName = mergedSkillsConfig.readToolName ?? "read_file";
703
+ const catalogCtx = { catalog: buildCatalog(resolvedSkills, readToolName), skills: resolvedSkills };
704
+ await hooks.callHook("skills:catalog", catalogCtx);
705
+ skillsCatalog = catalogCtx.catalog;
706
+ const fsSkills = resolvedSkills.filter((s) => s.location);
707
+ if (fsSkills.length > 0) {
708
+ const skillByLocation = new Map(fsSkills.map((s) => [s.location, s]));
709
+ hooks.hook("tool:after", (ctx) => {
710
+ if (ctx.name !== readToolName)
711
+ return;
712
+ const path = ctx.input.path;
713
+ if (!path)
714
+ return;
715
+ const matched = skillByLocation.get(path) ?? fsSkills.find((s) => s.location.endsWith(path));
716
+ if (matched)
717
+ hooks.callHook("skills:activate", { skill: matched });
718
+ });
719
+ }
720
+ }
487
721
  const thinking = options.thinking ?? "off";
488
722
  const model = options.model ?? provider.meta.defaultModel;
489
- const system = options.system || harness.system || "You are a helpful assistant.";
490
- const tools = mcpConnection ? { ...harness.tools, ...mcpConnection.tools } : harness.tools;
723
+ let system = options.system || harness.system || "You are a helpful assistant.";
724
+ if (skillsCatalog) {
725
+ system = `${system}
726
+
727
+ ${skillsCatalog}`;
728
+ }
729
+ const harnessTools = enableTools ? mcpConnection ? { ...harness.tools, ...mcpConnection.tools } : harness.tools : {};
730
+ const tools = {};
731
+ for (const tool of Object.values(harnessTools)) {
732
+ tools[tool.spec.name] = tool;
733
+ }
491
734
  const toolSpecs = Object.values(tools).map(
492
735
  (t) => ({
493
736
  name: t.spec.name,
@@ -495,19 +738,45 @@ function createAgent({ harness, provider, toolExecution = "sequential", executio
495
738
  input_schema: t.spec.input_schema
496
739
  })
497
740
  );
498
- const formattedTools = provider.formatTools(toolSpecs);
499
- const messages = [];
741
+ const formattedTools = enableTools ? provider.formatTools(toolSpecs) : [];
742
+ const turns = [];
743
+ const isResume = session && session.turns.length > 0 && session.runs.length > 0;
744
+ if (isResume) {
745
+ turns.push(...session.turns);
746
+ }
500
747
  if (options.system) {
501
748
  await hooks.callHook("system:before", { system: options.system });
502
- messages.push(provider.userMessage(options.system));
503
- messages.push(provider.assistantMessage("Understood. I will proceed with these instructions above the rest of my system prompt."));
749
+ const systemUserMsg = provider.userMessage(options.system);
750
+ turns.push({
751
+ id: crypto.randomUUID(),
752
+ role: systemUserMsg.role,
753
+ content: systemUserMsg.content,
754
+ createdAt: Date.now()
755
+ });
756
+ const systemAckMsg = provider.assistantMessage("Understood. I will proceed with these instructions above the rest of my system prompt.");
757
+ turns.push({
758
+ id: crypto.randomUUID(),
759
+ role: systemAckMsg.role,
760
+ content: systemAckMsg.content,
761
+ createdAt: Date.now()
762
+ });
504
763
  }
505
- messages.push(provider.userMessage(options.prompt, options.images));
506
- conversationMessages = messages;
507
- session?.setMessages(messages);
508
- const unregisterSessionSync = session ? hooks.hook("turn:after", () => {
509
- session.setMessages(messages);
510
- hooks.callHook("session:messages", { sessionId: session.id, count: messages.length });
764
+ const promptMsg = provider.userMessage(options.prompt, options.images);
765
+ turns.push({
766
+ id: crypto.randomUUID(),
767
+ role: promptMsg.role,
768
+ content: promptMsg.content,
769
+ createdAt: Date.now()
770
+ });
771
+ conversationTurns = turns;
772
+ let lastPersistedTurnCount = isResume ? session.turns.length : 0;
773
+ const unregisterSessionSync = session ? hooks.hook("turn:after", async () => {
774
+ const newTurns = turns.slice(lastPersistedTurnCount);
775
+ if (newTurns.length > 0) {
776
+ await session.appendTurns(newTurns);
777
+ lastPersistedTurnCount = turns.length;
778
+ hooks.callHook("session:turns", { sessionId: session.id, count: turns.length });
779
+ }
511
780
  }) : void 0;
512
781
  try {
513
782
  const stats = await runLoop({
@@ -526,7 +795,8 @@ function createAgent({ harness, provider, toolExecution = "sequential", executio
526
795
  handle: executionHandle,
527
796
  steeringQueue,
528
797
  followUpQueue,
529
- messages
798
+ turns,
799
+ generateTurnId: () => session?.generateTurnId() ?? crypto.randomUUID()
530
800
  });
531
801
  const finalStats = {
532
802
  ...stats,
@@ -534,15 +804,24 @@ function createAgent({ harness, provider, toolExecution = "sequential", executio
534
804
  };
535
805
  if (abortController.signal.aborted) {
536
806
  session?.abortRun(runId);
537
- if (session)
807
+ if (session) {
808
+ const run2 = session.runs.find((r) => r.id === runId);
809
+ if (run2)
810
+ await session.updateRun(run2);
811
+ await session.updateStatus("idle");
538
812
  await hooks.callHook("session:end", { sessionId: session.id, runId, status: "aborted" });
813
+ }
539
814
  await hooks.callHook("agent:done", finalStats);
540
815
  return finalStats;
541
816
  }
542
817
  const totalCost = finalStats.turnUsage?.reduce((sum, t) => sum + (t.cost ?? 0), 0) || void 0;
543
818
  if (totalCost)
544
819
  finalStats.cost = totalCost;
545
- session?.setMessages(messages);
820
+ const finalNewTurns = turns.slice(lastPersistedTurnCount);
821
+ if (session && finalNewTurns.length > 0) {
822
+ await session.appendTurns(finalNewTurns);
823
+ lastPersistedTurnCount = turns.length;
824
+ }
546
825
  session?.completeRun(runId, {
547
826
  turns: finalStats.turns,
548
827
  tokensIn: finalStats.totalIn,
@@ -550,22 +829,37 @@ function createAgent({ harness, provider, toolExecution = "sequential", executio
550
829
  turnUsage: finalStats.turnUsage,
551
830
  cost: totalCost
552
831
  });
553
- if (session)
832
+ if (session) {
833
+ const run2 = session.runs.find((r) => r.id === runId);
834
+ if (run2)
835
+ await session.updateRun(run2);
836
+ await session.updateStatus("completed");
554
837
  await hooks.callHook("session:end", { sessionId: session.id, runId, status: "completed" });
838
+ }
555
839
  await hooks.callHook("agent:done", finalStats);
556
840
  return finalStats;
557
841
  } catch (err) {
558
842
  if (abortController.signal.aborted) {
559
843
  session?.abortRun(runId);
560
- if (session)
844
+ if (session) {
845
+ const run2 = session.runs.find((r) => r.id === runId);
846
+ if (run2)
847
+ await session.updateRun(run2);
848
+ await session.updateStatus("idle");
561
849
  await hooks.callHook("session:end", { sessionId: session.id, runId, status: "aborted" });
850
+ }
562
851
  const stats = { totalIn: 0, totalOut: 0, turns: 0, elapsed: 0 };
563
852
  await hooks.callHook("agent:done", stats);
564
853
  return stats;
565
854
  }
566
855
  session?.errorRun(runId, err.message);
567
- if (session)
856
+ if (session) {
857
+ const run2 = session.runs.find((r) => r.id === runId);
858
+ if (run2)
859
+ await session.updateRun(run2);
860
+ await session.updateStatus("error");
568
861
  await hooks.callHook("session:end", { sessionId: session.id, runId, status: "error" });
862
+ }
569
863
  throw err;
570
864
  } finally {
571
865
  unregisterSpawnHook();
@@ -592,7 +886,7 @@ function createAgent({ harness, provider, toolExecution = "sequential", executio
592
886
  return idlePromise ?? Promise.resolve();
593
887
  }
594
888
  function reset() {
595
- conversationMessages = [];
889
+ conversationTurns = [];
596
890
  steeringQueue.length = 0;
597
891
  followUpQueue.length = 0;
598
892
  }
@@ -630,8 +924,8 @@ function createAgent({ harness, provider, toolExecution = "sequential", executio
630
924
  get isRunning() {
631
925
  return running;
632
926
  },
633
- get messages() {
634
- return conversationMessages;
927
+ get turns() {
928
+ return conversationTurns;
635
929
  },
636
930
  get execution() {
637
931
  return executionContext;
@@ -649,8 +943,12 @@ var init_agent = __esm({
649
943
  "src/agent.ts"() {
650
944
  "use strict";
651
945
  init_contexts();
946
+ init_harnesses();
652
947
  init_loop();
653
948
  init_mcp();
949
+ init_catalog();
950
+ init_interpolate();
951
+ init_resolve();
654
952
  }
655
953
  });
656
954
 
@@ -660,6 +958,17 @@ __export(spawn_exports, {
660
958
  createSpawnTool: () => createSpawnTool,
661
959
  spawn: () => spawn
662
960
  });
961
+ function extractText(message) {
962
+ if (!message || typeof message !== "object")
963
+ return "";
964
+ const msg = message;
965
+ if (typeof msg.content === "string")
966
+ return msg.content;
967
+ if (Array.isArray(msg.content)) {
968
+ return msg.content.filter((block) => block.type === "text").map((block) => block.text).join("\n");
969
+ }
970
+ return "";
971
+ }
663
972
  function createSpawnTool(options = {}) {
664
973
  const localChildren = /* @__PURE__ */ new Map();
665
974
  let localCounter = 0;
@@ -731,7 +1040,7 @@ function createSpawnTool(options = {}) {
731
1040
  task,
732
1041
  stats
733
1042
  });
734
- const response = extractText(agent.messages.at(-1));
1043
+ const response = extractText(agent.turns.at(-1));
735
1044
  return [
736
1045
  `[sub-agent ${id}] Completed in ${stats.turns} turns (${stats.elapsed}ms)`,
737
1046
  `Tokens: ${stats.totalIn} in / ${stats.totalOut} out`,
@@ -749,257 +1058,71 @@ function createSpawnTool(options = {}) {
749
1058
  }
750
1059
  };
751
1060
  }
752
- function extractText(message) {
753
- if (!message || typeof message !== "object")
754
- return "";
755
- const msg = message;
756
- if (typeof msg.content === "string")
757
- return msg.content;
758
- if (Array.isArray(msg.content)) {
759
- return msg.content.filter((block) => block.type === "text").map((block) => block.text).join("\n");
760
- }
761
- return "";
762
- }
763
- var children, childCounter, activeCount, MAX_CONCURRENT, _totalChildStats, spawn;
1061
+ var spawn;
764
1062
  var init_spawn = __esm({
765
1063
  "src/tools/spawn.ts"() {
766
1064
  "use strict";
767
1065
  init_agent();
768
- children = /* @__PURE__ */ new Map();
769
- childCounter = 0;
770
- activeCount = 0;
771
- MAX_CONCURRENT = 3;
772
- _totalChildStats = {
773
- totalIn: 0,
774
- totalOut: 0,
775
- turns: 0,
776
- elapsed: 0
777
- };
778
- spawn = {
779
- get children() {
780
- return children;
781
- },
782
- get totalChildStats() {
783
- return { ..._totalChildStats };
784
- },
1066
+ spawn = createSpawnTool();
1067
+ }
1068
+ });
1069
+
1070
+ // src/tools/write-file.ts
1071
+ var writeFile2;
1072
+ var init_write_file = __esm({
1073
+ "src/tools/write-file.ts"() {
1074
+ "use strict";
1075
+ writeFile2 = {
785
1076
  spec: {
786
- name: "spawn",
787
- description: "Spawn a sub-agent to work on a specific task. The sub-agent runs independently with its own tool access and returns its final response. Use this to delegate work, parallelize tasks, or isolate concerns.",
1077
+ name: "write_file",
1078
+ description: "Write content to a file. Creates parent directories if needed.",
788
1079
  input_schema: {
789
1080
  type: "object",
790
1081
  properties: {
791
- task: {
792
- type: "string",
793
- description: "The task prompt for the sub-agent. Be specific about what you want it to accomplish."
794
- },
795
- system: {
796
- type: "string",
797
- description: "Optional system prompt override for this specific sub-agent."
798
- }
1082
+ path: { type: "string", description: "Relative file path" },
1083
+ content: { type: "string", description: "File content to write" }
799
1084
  },
800
- required: ["task"]
1085
+ required: ["path", "content"]
801
1086
  }
802
1087
  },
803
- async execute(input, ctx) {
804
- const task = input.task;
805
- const systemOverride = input.system;
806
- if (activeCount >= MAX_CONCURRENT) {
807
- return `Cannot spawn: ${activeCount}/${MAX_CONCURRENT} sub-agents already running. Wait for one to complete.`;
808
- }
809
- const id = `child-${++childCounter}`;
810
- const child = { id, task, startedAt: Date.now() };
811
- const agent = createAgent({
812
- harness: ctx.harness,
813
- provider: ctx.provider,
814
- execution: ctx.execution
815
- });
816
- children.set(id, child);
817
- activeCount++;
818
- await ctx.hooks.callHook("spawn:before", { id, task });
819
- try {
820
- const stats = await agent.run({
821
- prompt: task,
822
- system: systemOverride,
823
- signal: ctx.signal
824
- });
825
- _totalChildStats.totalIn += stats.totalIn;
826
- _totalChildStats.totalOut += stats.totalOut;
827
- _totalChildStats.turns += stats.turns;
828
- _totalChildStats.elapsed += stats.elapsed;
829
- await ctx.hooks.callHook("spawn:complete", {
830
- id,
831
- task,
832
- stats
833
- });
834
- const response = extractText(agent.messages.at(-1));
835
- return [
836
- `[sub-agent ${id}] Completed in ${stats.turns} turns (${stats.elapsed}ms)`,
837
- `Tokens: ${stats.totalIn} in / ${stats.totalOut} out`,
838
- "",
839
- response || "(no text response)"
840
- ].join("\n");
841
- } catch (err) {
842
- await ctx.hooks.callHook("spawn:error", { id, task, error: err });
843
- return `[sub-agent ${id}] Error: ${err.message}`;
844
- } finally {
845
- activeCount--;
846
- await agent.destroy();
847
- children.delete(id);
848
- }
1088
+ async execute({ path, content }, ctx) {
1089
+ await ctx.execution.writeFile(ctx.handle, path, content);
1090
+ return `Wrote ${content.length} bytes to ${path}`;
849
1091
  }
850
1092
  };
851
1093
  }
852
1094
  });
853
1095
 
854
- // src/tools/list-files.ts
855
- import { existsSync, readdirSync, statSync } from "fs";
856
- import { resolve } from "path";
857
- var cwd = process.cwd();
858
- function safePath(p) {
859
- const resolved = resolve(cwd, p);
860
- if (!resolved.startsWith(cwd))
861
- throw new Error(`Path escapes working directory: ${p}`);
862
- return resolved;
863
- }
864
- var listFiles = {
865
- spec: {
866
- name: "list_files",
867
- description: "List files and directories at the given path (relative to project root).",
868
- input_schema: {
869
- type: "object",
870
- properties: {
871
- path: { type: "string", description: 'Relative directory path (default: ".")' }
872
- },
873
- required: []
874
- }
875
- },
876
- async execute({ path }, _ctx) {
877
- const target = safePath(path || ".");
878
- if (!existsSync(target))
879
- return `Directory not found: ${path}`;
880
- const entries = readdirSync(target);
881
- return entries.map((name) => {
882
- const full = resolve(target, name);
883
- const isDir = statSync(full).isDirectory();
884
- return `${isDir ? "\u{1F4C1}" : "\u{1F4C4}"} ${name}`;
885
- }).join("\n");
886
- }
887
- };
888
-
889
- // src/tools/read-file.ts
890
- import { existsSync as existsSync2, readFileSync } from "fs";
891
- import { resolve as resolve2 } from "path";
892
- var cwd2 = process.cwd();
893
- function safePath2(p) {
894
- const resolved = resolve2(cwd2, p);
895
- if (!resolved.startsWith(cwd2))
896
- throw new Error(`Path escapes working directory: ${p}`);
897
- return resolved;
898
- }
899
- var readFile = {
900
- spec: {
901
- name: "read_file",
902
- description: "Read the contents of a file at the given path (relative to project root).",
903
- input_schema: {
904
- type: "object",
905
- properties: {
906
- path: { type: "string", description: "Relative file path" }
907
- },
908
- required: ["path"]
909
- }
910
- },
911
- async execute({ path }, _ctx) {
912
- const target = safePath2(path);
913
- if (!existsSync2(target))
914
- return `File not found: ${path}`;
915
- return readFileSync(target, "utf-8");
916
- }
917
- };
918
-
919
- // src/tools/shell.ts
920
- import { execSync } from "child_process";
921
- var cwd3 = process.cwd();
922
- var shell = {
923
- spec: {
924
- name: "shell",
925
- description: "Execute a shell command and return stdout+stderr. Runs in the project root.",
926
- input_schema: {
927
- type: "object",
928
- properties: {
929
- command: { type: "string", description: "The shell command to run" }
930
- },
931
- required: ["command"]
932
- }
933
- },
934
- async execute({ command }, _ctx) {
935
- try {
936
- const out = execSync(command, {
937
- cwd: cwd3,
938
- encoding: "utf-8",
939
- timeout: 3e4,
940
- maxBuffer: 1024 * 1024,
941
- stdio: ["pipe", "pipe", "pipe"]
942
- });
943
- return out || "(no output)";
944
- } catch (err) {
945
- const stderr = err.stderr?.toString() ?? "";
946
- const stdout = err.stdout?.toString() ?? "";
947
- return `Exit code ${err.status ?? 1}
948
- ${stdout}
949
- ${stderr}`.trim();
950
- }
951
- }
952
- };
953
-
954
1096
  // src/tools/index.ts
955
- init_spawn();
956
- init_validation();
957
-
958
- // src/tools/write-file.ts
959
- import { mkdirSync, writeFileSync } from "fs";
960
- import { dirname as dirname2, resolve as resolve4 } from "path";
961
- var cwd4 = process.cwd();
962
- function safePath3(p) {
963
- const resolved = resolve4(cwd4, p);
964
- if (!resolved.startsWith(cwd4))
965
- throw new Error(`Path escapes working directory: ${p}`);
966
- return resolved;
967
- }
968
- var writeFile2 = {
969
- spec: {
970
- name: "write_file",
971
- description: "Write content to a file. Creates parent directories if needed.",
972
- input_schema: {
973
- type: "object",
974
- properties: {
975
- path: { type: "string", description: "Relative file path" },
976
- content: { type: "string", description: "File content to write" }
977
- },
978
- required: ["path", "content"]
979
- }
980
- },
981
- async execute({ path, content }, _ctx) {
982
- const target = safePath3(path);
983
- mkdirSync(dirname2(target), { recursive: true });
984
- writeFileSync(target, content);
985
- return `Wrote ${content.length} bytes to ${path}`;
1097
+ var init_tools = __esm({
1098
+ "src/tools/index.ts"() {
1099
+ init_list_files();
1100
+ init_read_file();
1101
+ init_shell();
1102
+ init_spawn();
1103
+ init_validation();
1104
+ init_write_file();
986
1105
  }
987
- };
1106
+ });
988
1107
 
989
1108
  export {
990
1109
  createDockerContext,
991
1110
  createProcessContext,
992
1111
  createSandboxContext,
993
1112
  init_contexts,
994
- validateToolArgs,
995
- createAgent,
996
- init_agent,
997
1113
  listFiles,
998
1114
  readFile,
999
1115
  shell,
1000
- spawn,
1001
1116
  createSpawnTool,
1002
- spawn_exports,
1003
- init_spawn,
1004
- writeFile2 as writeFile
1117
+ spawn,
1118
+ validateToolArgs,
1119
+ writeFile2 as writeFile,
1120
+ init_tools,
1121
+ basicTools,
1122
+ basic_default,
1123
+ defineHarness,
1124
+ noTools,
1125
+ init_harnesses,
1126
+ createAgent,
1127
+ init_agent
1005
1128
  };