zidane 2.0.1 → 2.2.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.
Files changed (36) hide show
  1. package/README.md +40 -26
  2. package/dist/{agent-D-ZFMbSd.d.ts → agent-vPBFXnu-.d.ts} +389 -274
  3. package/dist/{chunk-SZA4FKW5.js → chunk-2EQT4EHD.js} +4 -3
  4. package/dist/{chunk-PJUUYBKF.js → chunk-37GD7NL3.js} +45 -16
  5. package/dist/{chunk-LVC7NQUZ.js → chunk-BW3WTFIR.js} +1 -1
  6. package/dist/{chunk-FRNFVKWW.js → chunk-CDRXC7A7.js} +64 -33
  7. package/dist/{chunk-PASFWG7S.js → chunk-F5UBXERT.js} +309 -77
  8. package/dist/{chunk-7JTBBZ2U.js → chunk-LNN5UTS2.js} +8 -0
  9. package/dist/{chunk-VG2E6YK3.js → chunk-PMCQOMV4.js} +4 -2
  10. package/dist/{chunk-LN4LLLHA.js → chunk-S3FCOMRI.js} +63 -20
  11. package/dist/{chunk-OVQ4N64O.js → chunk-SP5NA6WF.js} +6 -12
  12. package/dist/{chunk-BCXXXJ3G.js → chunk-TPXPVEH6.js} +99 -58
  13. package/dist/contexts.js +1 -1
  14. package/dist/index.d.ts +6 -5
  15. package/dist/index.js +16 -16
  16. package/dist/mcp.d.ts +1 -1
  17. package/dist/mcp.js +1 -1
  18. package/dist/presets.d.ts +33 -0
  19. package/dist/presets.js +15 -0
  20. package/dist/providers.d.ts +1 -1
  21. package/dist/providers.js +3 -3
  22. package/dist/session/sqlite.d.ts +1 -1
  23. package/dist/session.d.ts +1 -1
  24. package/dist/session.js +3 -3
  25. package/dist/{skills-use-C4KFVla0.d.ts → skills-use-39cCsA7_.d.ts} +4 -4
  26. package/dist/skills.d.ts +3 -9
  27. package/dist/skills.js +3 -5
  28. package/dist/spawn-Czx3owjX.d.ts +152 -0
  29. package/dist/tools.d.ts +6 -4
  30. package/dist/tools.js +5 -5
  31. package/dist/types.d.ts +3 -2
  32. package/dist/types.js +1 -1
  33. package/package.json +5 -5
  34. package/dist/harnesses.d.ts +0 -4
  35. package/dist/harnesses.js +0 -17
  36. package/dist/spawn-RoqpjYLZ.d.ts +0 -99
@@ -3,21 +3,20 @@ import {
3
3
  createSkillActivationState,
4
4
  installAllowedToolsGate,
5
5
  interpolateShellCommands,
6
- mergeSkillsConfig,
7
6
  resolveSkills,
8
7
  validateResourcePath
9
- } from "./chunk-BCXXXJ3G.js";
8
+ } from "./chunk-TPXPVEH6.js";
10
9
  import {
11
10
  createProcessContext
12
- } from "./chunk-SZA4FKW5.js";
11
+ } from "./chunk-2EQT4EHD.js";
13
12
  import {
14
13
  connectMcpServers
15
- } from "./chunk-PJUUYBKF.js";
14
+ } from "./chunk-37GD7NL3.js";
16
15
  import {
17
16
  AgentAbortedError,
18
17
  AgentProviderError,
19
18
  toTypedError
20
- } from "./chunk-7JTBBZ2U.js";
19
+ } from "./chunk-LNN5UTS2.js";
21
20
 
22
21
  // src/tools/glob.ts
23
22
  var DEFAULT_LIMIT = 1e3;
@@ -69,7 +68,8 @@ var glob = {
69
68
  const entries = ctx.execution.type === "process" ? await globInProcess(pat, ctx.handle.cwd, max) : await globViaShell(pat, ctx, max);
70
69
  return entries.length > 0 ? entries.join("\n") : "(no matches)";
71
70
  } catch (err) {
72
- return `Glob error: ${err.message}`;
71
+ const message = err instanceof Error ? err.message : String(err);
72
+ return `Glob error: ${message}`;
73
73
  }
74
74
  }
75
75
  };
@@ -211,7 +211,8 @@ function createSkillsReadTool(options) {
211
211
  try {
212
212
  content = await ctx.execution.readFile(ctx.handle, validated.absolutePath);
213
213
  } catch (err) {
214
- return `Error reading "${relPath}" in skill "${skillName}": ${err.message}`;
214
+ const message = err instanceof Error ? err.message : String(err);
215
+ return `Error reading "${relPath}" in skill "${skillName}": ${message}`;
215
216
  }
216
217
  if (looksBinary(content)) {
217
218
  return JSON.stringify({
@@ -289,7 +290,8 @@ function createSkillsRunScriptTool(options) {
289
290
  stderr: result.stderr
290
291
  });
291
292
  } catch (err) {
292
- return `Error running script "${scriptRel}" for skill "${skillName}": ${err.message}`;
293
+ const message = err instanceof Error ? err.message : String(err);
294
+ return `Error running script "${scriptRel}" for skill "${skillName}": ${message}`;
293
295
  }
294
296
  }
295
297
  };
@@ -772,21 +774,31 @@ async function executeSingleTool(ctx, call, turnId) {
772
774
  execution: ctx.execution,
773
775
  handle: ctx.handle,
774
776
  hooks: ctx.hooks,
775
- harness: ctx.harness,
777
+ tools: ctx.agentTools,
778
+ ...ctx.agentName !== void 0 ? { name: ctx.agentName } : {},
779
+ ...ctx.agentSystem !== void 0 ? { system: ctx.agentSystem } : {},
780
+ ...ctx.agentToolAliases !== void 0 ? { toolAliases: ctx.agentToolAliases } : {},
781
+ ...ctx.agentMcpServers !== void 0 ? { mcpServers: ctx.agentMcpServers } : {},
782
+ ...ctx.agentSkills !== void 0 ? { skills: ctx.agentSkills } : {},
783
+ ...ctx.agentBehavior !== void 0 ? { behavior: ctx.agentBehavior } : {},
776
784
  turnId,
777
- callId
785
+ callId,
786
+ runId: ctx.runId,
787
+ ...ctx.session ? { session: ctx.session } : {},
788
+ ...typeof ctx.depth === "number" ? { depth: ctx.depth } : {}
778
789
  };
779
790
  output = await toolDef.execute(effectiveInput, toolCtx);
780
791
  } catch (err) {
792
+ const error = err instanceof Error ? err : new Error(String(err));
781
793
  await ctx.hooks.callHook("tool:error", {
782
794
  turnId,
783
795
  callId,
784
796
  name: call.name,
785
797
  displayName,
786
798
  input: effectiveInput,
787
- error: err
799
+ error
788
800
  });
789
- output = `Tool error: ${err.message}`;
801
+ output = `Tool error: ${error.message}`;
790
802
  isError = true;
791
803
  }
792
804
  const transformCtx = {
@@ -920,38 +932,88 @@ function buildPromptMessage(provider, parts) {
920
932
  }
921
933
 
922
934
  // src/agent.ts
923
- var noTools = { name: "none", system: "You are a helpful assistant.", tools: {} };
924
- function resolveBehavior(harnessBehavior, agentBehavior, runBehavior) {
935
+ var HOOK_EVENT_NAMES = [
936
+ "system:before",
937
+ "turn:before",
938
+ "turn:after",
939
+ "stream:text",
940
+ "stream:end",
941
+ "stream:thinking",
942
+ "oauth:refresh",
943
+ "tool:gate",
944
+ "tool:before",
945
+ "tool:after",
946
+ "tool:error",
947
+ "tool:transform",
948
+ "context:transform",
949
+ "steer:inject",
950
+ "spawn:before",
951
+ "spawn:complete",
952
+ "spawn:error",
953
+ "child:stream:text",
954
+ "child:stream:thinking",
955
+ "child:stream:end",
956
+ "child:tool:before",
957
+ "child:tool:after",
958
+ "child:tool:error",
959
+ "child:turn:after",
960
+ "mcp:connect",
961
+ "mcp:error",
962
+ "mcp:close",
963
+ "mcp:tool:gate",
964
+ "mcp:tool:before",
965
+ "mcp:tool:after",
966
+ "mcp:tool:transform",
967
+ "mcp:tool:error",
968
+ "skills:resolve",
969
+ "skills:catalog",
970
+ "skills:activate",
971
+ "skills:deactivate",
972
+ "usage",
973
+ "output",
974
+ "agent:abort",
975
+ "agent:done",
976
+ "session:start",
977
+ "session:end",
978
+ "session:turns",
979
+ "session:meta",
980
+ "session:save"
981
+ ];
982
+ var HOOK_EVENT_SET = new Set(HOOK_EVENT_NAMES);
983
+ function isKnownHookEvent(event) {
984
+ return HOOK_EVENT_SET.has(event);
985
+ }
986
+ function resolveBehavior(agentBehavior, runBehavior) {
925
987
  return {
926
- toolExecution: runBehavior?.toolExecution ?? agentBehavior?.toolExecution ?? harnessBehavior?.toolExecution ?? "parallel",
927
- maxTurns: runBehavior?.maxTurns ?? agentBehavior?.maxTurns ?? harnessBehavior?.maxTurns,
928
- maxTokens: runBehavior?.maxTokens ?? agentBehavior?.maxTokens ?? harnessBehavior?.maxTokens,
929
- thinkingBudget: runBehavior?.thinkingBudget ?? agentBehavior?.thinkingBudget ?? harnessBehavior?.thinkingBudget,
930
- schema: runBehavior?.schema ?? agentBehavior?.schema ?? harnessBehavior?.schema
988
+ toolExecution: runBehavior?.toolExecution ?? agentBehavior?.toolExecution ?? "parallel",
989
+ maxTurns: runBehavior?.maxTurns ?? agentBehavior?.maxTurns,
990
+ maxTokens: runBehavior?.maxTokens ?? agentBehavior?.maxTokens,
991
+ thinkingBudget: runBehavior?.thinkingBudget ?? agentBehavior?.thinkingBudget,
992
+ schema: runBehavior?.schema ?? agentBehavior?.schema
931
993
  };
932
994
  }
933
- function createAgent({ harness: harnessOption, provider, behavior: agentBehavior, execution, mcpServers, session, skills: agentSkills, _mcpConnector }) {
995
+ function createAgent({ provider, name: agentName, system: agentSystem, tools: agentTools, toolAliases, behavior: agentBehavior, execution, mcpServers, session, skills: agentSkills, _mcpConnector }) {
934
996
  const hooks = createHooks();
935
- const harness = harnessOption ?? noTools;
936
997
  const executionContext = execution ?? createProcessContext();
998
+ const sourceTools = agentTools ?? {};
937
999
  let abortController;
938
1000
  let running = false;
939
1001
  let idleResolve;
940
1002
  let idlePromise;
941
1003
  let executionHandle = null;
942
1004
  let mcpConnection = null;
943
- const allMcpServers = [...harness.mcpServers ?? [], ...mcpServers ?? []];
1005
+ const allMcpServers = mcpServers ?? [];
944
1006
  const steeringQueue = [];
945
1007
  const followUpQueue = [];
946
1008
  let conversationTurns = session?.turns.slice() ?? [];
947
1009
  let runCounter = session?.runs.length ?? 0;
948
- const mergedSkillsConfig = mergeSkillsConfig(harness.skills, agentSkills);
949
- const skillsEnabledValue = mergedSkillsConfig?.enabled;
1010
+ const skillsConfig = agentSkills;
1011
+ const skillsEnabledValue = skillsConfig?.enabled;
950
1012
  const skillsDisabled = skillsEnabledValue === false || Array.isArray(skillsEnabledValue) && skillsEnabledValue.length === 0;
951
1013
  let resolvedSkills = null;
952
1014
  let skillsCatalog = null;
953
1015
  const skillActivationState = createSkillActivationState({
954
- maxActive: mergedSkillsConfig?.maxActive
1016
+ maxActive: skillsConfig?.maxActive
955
1017
  });
956
1018
  async function run(options) {
957
1019
  if (running) {
@@ -971,7 +1033,10 @@ function createAgent({ harness: harnessOption, provider, behavior: agentBehavior
971
1033
  abortController = new AbortController();
972
1034
  const runId = `run_${++runCounter}`;
973
1035
  const promptLabel = typeof options.prompt === "string" ? options.prompt : Array.isArray(options.prompt) ? options.prompt.filter((p) => p.type === "text").map((p) => p.text).join("\n") : "";
974
- session?.startRun(runId, promptLabel);
1036
+ session?.startRun(runId, promptLabel, {
1037
+ ...options.parentRunId ? { parentRunId: options.parentRunId } : {},
1038
+ depth: typeof options.depth === "number" ? options.depth : 0
1039
+ });
975
1040
  if (session) {
976
1041
  await session.updateStatus("running");
977
1042
  await hooks.callHook("session:start", { sessionId: session.id, runId, prompt: promptLabel });
@@ -994,6 +1059,11 @@ function createAgent({ harness: harnessOption, provider, behavior: agentBehavior
994
1059
  const perRunUnregisters = [];
995
1060
  if (options.hooks) {
996
1061
  for (const [event, handler] of Object.entries(options.hooks)) {
1062
+ if (!isKnownHookEvent(event)) {
1063
+ throw new Error(
1064
+ `Unknown hook event "${event}" passed to run(). See AgentHooks for valid events.`
1065
+ );
1066
+ }
997
1067
  const handlerList = Array.isArray(handler) ? handler : [handler];
998
1068
  for (const fn of handlerList) {
999
1069
  if (typeof fn !== "function")
@@ -1012,10 +1082,10 @@ function createAgent({ harness: harnessOption, provider, behavior: agentBehavior
1012
1082
  mcpConnection = await connectMcpServers(allMcpServers, void 0, hooks);
1013
1083
  }
1014
1084
  }
1015
- if (!skillsDisabled && mergedSkillsConfig && !resolvedSkills) {
1016
- resolvedSkills = await resolveSkills(mergedSkillsConfig);
1085
+ if (!skillsDisabled && skillsConfig && !resolvedSkills) {
1086
+ resolvedSkills = await resolveSkills(skillsConfig);
1017
1087
  await hooks.callHook("skills:resolve", { skills: resolvedSkills });
1018
- const skillsToolRegistered = mergedSkillsConfig?.tool !== false && resolvedSkills.length > 0;
1088
+ const skillsToolRegistered = skillsConfig?.tool !== false && resolvedSkills.length > 0;
1019
1089
  const catalogCtx = {
1020
1090
  catalog: buildCatalog(resolvedSkills, { skillsToolRegistered }),
1021
1091
  skills: resolvedSkills
@@ -1045,17 +1115,17 @@ function createAgent({ harness: harnessOption, provider, behavior: agentBehavior
1045
1115
  }
1046
1116
  const thinking = options.thinking ?? "off";
1047
1117
  const model = options.model ?? provider.meta.defaultModel;
1048
- const { toolExecution, maxTurns, maxTokens, thinkingBudget, schema } = resolveBehavior(harness.behavior, agentBehavior, options.behavior);
1049
- let system = options.system || harness.system || "You are a helpful assistant.";
1118
+ const { toolExecution, maxTurns, maxTokens, thinkingBudget, schema } = resolveBehavior(agentBehavior, options.behavior);
1119
+ let system = options.system || agentSystem || "You are a helpful assistant.";
1050
1120
  if (skillsCatalog) {
1051
1121
  system = `${system}
1052
1122
 
1053
1123
  ${skillsCatalog}`;
1054
1124
  }
1055
- const baseTools = options.tools !== void 0 ? options.tools : mcpConnection ? { ...harness.tools, ...mcpConnection.tools } : harness.tools;
1056
- const shouldInjectSkillTools = options.tools === void 0 && !!resolvedSkills && resolvedSkills.length > 0 && mergedSkillsConfig?.tool !== false;
1125
+ const runBaseTools = options.tools !== void 0 ? options.tools : mcpConnection ? { ...sourceTools, ...mcpConnection.tools } : sourceTools;
1126
+ const shouldInjectSkillTools = options.tools === void 0 && !!resolvedSkills && resolvedSkills.length > 0 && skillsConfig?.tool !== false;
1057
1127
  const mergedWithSkills = shouldInjectSkillTools ? {
1058
- // Auto-injected first so harness + MCP tools can override by name.
1128
+ // Auto-injected first so agent + MCP tools can override by name.
1059
1129
  skills_use: createSkillsUseTool({
1060
1130
  catalog: resolvedSkills,
1061
1131
  state: skillActivationState,
@@ -1068,15 +1138,15 @@ ${skillsCatalog}`;
1068
1138
  skills_run_script: createSkillsRunScriptTool({
1069
1139
  catalog: resolvedSkills,
1070
1140
  state: skillActivationState,
1071
- scriptTimeoutMs: mergedSkillsConfig?.scriptTimeoutMs
1141
+ scriptTimeoutMs: skillsConfig?.scriptTimeoutMs
1072
1142
  }),
1073
- ...baseTools
1074
- } : baseTools;
1143
+ ...runBaseTools
1144
+ } : runBaseTools;
1075
1145
  const tools = {};
1076
1146
  for (const tool of Object.values(mergedWithSkills)) {
1077
1147
  tools[tool.spec.name] = tool;
1078
1148
  }
1079
- const aliasMaps = buildAliasMaps(harness.toolAliases, Object.keys(tools));
1149
+ const aliasMaps = buildAliasMaps(toolAliases, Object.keys(tools));
1080
1150
  const toolSpecs = Object.values(tools).map(
1081
1151
  (t) => ({
1082
1152
  name: aliasMaps.aliasByCanonical.get(t.spec.name) ?? t.spec.name,
@@ -1140,11 +1210,18 @@ ${skillsCatalog}`;
1140
1210
  }
1141
1211
  const uninstallAllowedToolsGate = installAllowedToolsGate(hooks, skillActivationState);
1142
1212
  const runStartMs = Date.now();
1213
+ const runDepth = typeof options.depth === "number" ? options.depth : 0;
1143
1214
  try {
1144
1215
  const stats = await runLoop({
1145
1216
  provider,
1146
1217
  hooks,
1147
- harness,
1218
+ agentName,
1219
+ agentSystem,
1220
+ agentTools: sourceTools,
1221
+ agentToolAliases: toolAliases,
1222
+ agentMcpServers: mcpServers,
1223
+ agentSkills,
1224
+ agentBehavior,
1148
1225
  tools,
1149
1226
  formattedTools,
1150
1227
  aliasMaps,
@@ -1162,6 +1239,8 @@ ${skillsCatalog}`;
1162
1239
  generateTurnId: () => session?.generateTurnId() ?? crypto.randomUUID(),
1163
1240
  maxTurns,
1164
1241
  maxTokens,
1242
+ ...session ? { session } : {},
1243
+ depth: runDepth,
1165
1244
  thinkingBudget,
1166
1245
  schema,
1167
1246
  runStartMs
@@ -1199,7 +1278,8 @@ ${skillsCatalog}`;
1199
1278
  await hooks.callHook("agent:done", stats);
1200
1279
  return stats;
1201
1280
  }
1202
- session?.errorRun(runId, err.message);
1281
+ const message = err instanceof Error ? err.message : String(err);
1282
+ session?.errorRun(runId, message);
1203
1283
  await finalizeSession("error");
1204
1284
  throw err;
1205
1285
  } finally {
@@ -1230,12 +1310,13 @@ ${skillsCatalog}`;
1230
1310
  function waitForIdle() {
1231
1311
  return idlePromise ?? Promise.resolve();
1232
1312
  }
1233
- function reset() {
1313
+ async function reset() {
1234
1314
  conversationTurns = [];
1235
1315
  steeringQueue.length = 0;
1236
1316
  followUpQueue.length = 0;
1237
- for (const record of skillActivationState.clear())
1238
- void hooks.callHook("skills:deactivate", { skill: record.skill, reason: "reset" });
1317
+ const cleared = skillActivationState.clear();
1318
+ for (const record of cleared)
1319
+ await hooks.callHook("skills:deactivate", { skill: record.skill, reason: "reset" });
1239
1320
  }
1240
1321
  async function activateSkill(name) {
1241
1322
  if (!resolvedSkills) {
@@ -1251,7 +1332,7 @@ ${skillsCatalog}`;
1251
1332
  const outcome = skillActivationState.activate(skill, "explicit");
1252
1333
  if (outcome === "cap-reached") {
1253
1334
  throw new Error(
1254
- `Cannot activate skill "${name}" \u2014 the maxActive cap of ${mergedSkillsConfig?.maxActive} has been reached.`
1335
+ `Cannot activate skill "${name}" \u2014 the maxActive cap of ${skillsConfig?.maxActive} has been reached.`
1255
1336
  );
1256
1337
  }
1257
1338
  if (outcome === "ok")
@@ -1271,10 +1352,16 @@ ${skillsCatalog}`;
1271
1352
  };
1272
1353
  session.setMeta = (key, value) => {
1273
1354
  originalSetMeta(key, value);
1274
- hooks.callHook("session:meta", { sessionId: session.id, key, value });
1355
+ void Promise.resolve(hooks.callHook("session:meta", { sessionId: session.id, key, value })).catch((err) => {
1356
+ console.error("[zidane] session:meta listener rejected:", err);
1357
+ });
1275
1358
  };
1276
1359
  }
1360
+ let destroyed = false;
1277
1361
  async function destroy() {
1362
+ if (destroyed)
1363
+ return;
1364
+ destroyed = true;
1278
1365
  if (mcpConnection) {
1279
1366
  await mcpConnection.close();
1280
1367
  mcpConnection = null;
@@ -1313,11 +1400,34 @@ ${skillsCatalog}`;
1313
1400
  get activeSkills() {
1314
1401
  return skillActivationState.active();
1315
1402
  },
1316
- meta: provider.meta
1403
+ // Expose a frozen view of provider.meta. Hosts previously could mutate
1404
+ // the underlying provider meta (e.g. via `agent.meta.defaultModel = …`),
1405
+ // which quietly affected every other agent sharing the same provider
1406
+ // instance. Freezing forces callers to construct a new provider when
1407
+ // they want to override model/capabilities.
1408
+ meta: Object.freeze({ ...provider.meta })
1317
1409
  };
1318
1410
  }
1319
1411
 
1320
1412
  // src/tools/spawn.ts
1413
+ var BUBBLED_EVENTS = [
1414
+ "stream:text",
1415
+ "stream:thinking",
1416
+ "stream:end",
1417
+ "tool:before",
1418
+ "tool:after",
1419
+ "tool:error",
1420
+ "turn:after"
1421
+ ];
1422
+ var CHILD_EVENT_NAME = {
1423
+ "stream:text": "child:stream:text",
1424
+ "stream:thinking": "child:stream:thinking",
1425
+ "stream:end": "child:stream:end",
1426
+ "tool:before": "child:tool:before",
1427
+ "tool:after": "child:tool:after",
1428
+ "tool:error": "child:tool:error",
1429
+ "turn:after": "child:turn:after"
1430
+ };
1321
1431
  function extractText(message) {
1322
1432
  if (!message || typeof message !== "object")
1323
1433
  return "";
@@ -1325,15 +1435,60 @@ function extractText(message) {
1325
1435
  if (typeof msg.content === "string")
1326
1436
  return msg.content;
1327
1437
  if (Array.isArray(msg.content)) {
1328
- return msg.content.filter((block) => block.type === "text").map((block) => block.text).join("\n");
1438
+ return msg.content.filter((block) => !!block && typeof block === "object" && block.type === "text").map((block) => block.text).join("\n");
1329
1439
  }
1330
1440
  return "";
1331
1441
  }
1442
+ async function raceWithTimeout(task, timeoutMs) {
1443
+ if (!timeoutMs || timeoutMs <= 0)
1444
+ return task;
1445
+ let timer;
1446
+ try {
1447
+ return await new Promise((resolve, reject) => {
1448
+ timer = setTimeout(() => reject(new SpawnTimeoutError(timeoutMs)), timeoutMs);
1449
+ task.then(resolve, reject);
1450
+ });
1451
+ } finally {
1452
+ if (timer)
1453
+ clearTimeout(timer);
1454
+ }
1455
+ }
1456
+ var SpawnTimeoutError = class extends Error {
1457
+ timeoutMs;
1458
+ constructor(timeoutMs) {
1459
+ super(`Child agent timed out after ${timeoutMs}ms`);
1460
+ this.name = "SpawnTimeoutError";
1461
+ this.timeoutMs = timeoutMs;
1462
+ }
1463
+ };
1464
+ function bubbleHooks(childHooks, parentHooks, childId, depth) {
1465
+ const unregisters = [];
1466
+ const fire = parentHooks.callHook;
1467
+ for (const evt of BUBBLED_EVENTS) {
1468
+ const parentEvt = CHILD_EVENT_NAME[evt];
1469
+ const unregister = childHooks.hook(evt, (ctx) => {
1470
+ void fire(parentEvt, { ...ctx, childId, depth });
1471
+ });
1472
+ unregisters.push(unregister);
1473
+ }
1474
+ for (const evt of BUBBLED_EVENTS) {
1475
+ const parentEvt = CHILD_EVENT_NAME[evt];
1476
+ const unregister = childHooks.hook(parentEvt, (ctx) => {
1477
+ void fire(parentEvt, ctx);
1478
+ });
1479
+ unregisters.push(unregister);
1480
+ }
1481
+ return () => {
1482
+ for (const u of unregisters) u();
1483
+ };
1484
+ }
1332
1485
  function createSpawnTool(options = {}) {
1333
1486
  const localChildren = /* @__PURE__ */ new Map();
1334
1487
  let localCounter = 0;
1335
1488
  let localActiveCount = 0;
1336
1489
  const maxConcurrent = options.maxConcurrent ?? 3;
1490
+ const maxDepth = options.maxDepth ?? 3;
1491
+ const forwardHooks = options.forwardHooks ?? true;
1337
1492
  const localStats = {
1338
1493
  totalIn: 0,
1339
1494
  totalOut: 0,
@@ -1368,51 +1523,128 @@ function createSpawnTool(options = {}) {
1368
1523
  async execute(input, ctx) {
1369
1524
  const task = input.task;
1370
1525
  const systemOverride = input.system;
1526
+ const parentDepth = ctx.depth ?? 0;
1527
+ const childDepth = parentDepth + 1;
1528
+ if (childDepth > maxDepth) {
1529
+ return `Cannot spawn: maxDepth=${maxDepth} reached (parent depth=${parentDepth}). Deepen the cap with createSpawnTool({ maxDepth }).`;
1530
+ }
1371
1531
  if (localActiveCount >= maxConcurrent) {
1372
1532
  return `Cannot spawn: ${localActiveCount}/${maxConcurrent} sub-agents already running. Wait for one to complete.`;
1373
1533
  }
1534
+ if (ctx.signal.aborted) {
1535
+ return `[sub-agent pre-aborted] Parent signal was already aborted \u2014 skipped "${task.slice(0, 80)}"`;
1536
+ }
1374
1537
  const id = `child-${++localCounter}`;
1375
- const child = { id, task, startedAt: Date.now() };
1376
- const agent = createAgent({
1377
- harness: options.harness ?? ctx.harness,
1378
- provider: ctx.provider,
1379
- execution: ctx.execution
1380
- });
1381
- localChildren.set(id, child);
1382
1538
  localActiveCount++;
1383
- options.onSpawn?.(child);
1384
- await ctx.hooks.callHook("spawn:before", { id, task });
1539
+ const child = { id, task, startedAt: Date.now(), depth: childDepth };
1540
+ localChildren.set(id, child);
1541
+ let destroyError;
1542
+ let childRunStatus = "completed";
1543
+ let finalStats;
1544
+ let result = "";
1545
+ let unbubble;
1385
1546
  try {
1386
- const stats = await agent.run({
1547
+ const parentPreset = {
1548
+ ...ctx.name !== void 0 ? { name: ctx.name } : {},
1549
+ ...ctx.system !== void 0 ? { system: ctx.system } : {},
1550
+ tools: ctx.tools,
1551
+ ...ctx.toolAliases !== void 0 ? { toolAliases: ctx.toolAliases } : {},
1552
+ ...ctx.mcpServers !== void 0 ? { mcpServers: ctx.mcpServers } : {},
1553
+ ...ctx.skills !== void 0 ? { skills: ctx.skills } : {},
1554
+ ...ctx.behavior !== void 0 ? { behavior: ctx.behavior } : {}
1555
+ };
1556
+ const agent = createAgent({
1557
+ ...parentPreset,
1558
+ ...options.preset,
1559
+ provider: ctx.provider,
1560
+ execution: ctx.execution,
1561
+ // Share the parent's session on opt-in. Child turns get appended to
1562
+ // the same session.turns stream with the child's runId; the child
1563
+ // run itself is tagged with parentRunId below, via AgentRunOptions.
1564
+ ...options.persist && ctx.session ? { session: ctx.session } : {}
1565
+ });
1566
+ if (forwardHooks)
1567
+ unbubble = bubbleHooks(agent.hooks, ctx.hooks, id, childDepth);
1568
+ options.onSpawn?.(child);
1569
+ await ctx.hooks.callHook("spawn:before", { id, task, depth: childDepth });
1570
+ const runPromise = agent.run({
1387
1571
  prompt: task,
1388
1572
  model: options.model,
1389
1573
  system: systemOverride ?? options.system,
1390
1574
  thinking: options.thinking,
1391
- signal: ctx.signal
1575
+ signal: ctx.signal,
1576
+ depth: childDepth,
1577
+ ...options.persist && ctx.runId ? { parentRunId: ctx.runId } : {}
1392
1578
  });
1393
- localStats.totalIn += stats.totalIn;
1394
- localStats.totalOut += stats.totalOut;
1395
- localStats.turns += stats.turns;
1396
- localStats.elapsed += stats.elapsed;
1397
- options.onComplete?.(child, stats);
1398
- await ctx.hooks.callHook("spawn:complete", {
1579
+ try {
1580
+ finalStats = await raceWithTimeout(runPromise, options.timeoutMs);
1581
+ if (ctx.signal.aborted) {
1582
+ childRunStatus = "aborted";
1583
+ result = [
1584
+ `[sub-agent ${id}] Aborted after ${finalStats.turns} turns (${finalStats.elapsed}ms)`,
1585
+ `Tokens: ${finalStats.totalIn} in / ${finalStats.totalOut} out`
1586
+ ].join("\n");
1587
+ } else {
1588
+ const response = extractText(agent.turns.at(-1));
1589
+ result = [
1590
+ `[sub-agent ${id}] Completed in ${finalStats.turns} turns (${finalStats.elapsed}ms)`,
1591
+ `Tokens: ${finalStats.totalIn} in / ${finalStats.totalOut} out`,
1592
+ "",
1593
+ response || "(no text response)"
1594
+ ].join("\n");
1595
+ }
1596
+ } catch (err) {
1597
+ if (err instanceof SpawnTimeoutError) {
1598
+ childRunStatus = "timeout";
1599
+ agent.abort();
1600
+ try {
1601
+ finalStats = await runPromise;
1602
+ } catch {
1603
+ finalStats = { totalIn: 0, totalOut: 0, turns: 0, elapsed: err.timeoutMs };
1604
+ }
1605
+ result = `[sub-agent ${id}] Timed out after ${err.timeoutMs}ms`;
1606
+ } else {
1607
+ const error = err instanceof Error ? err : new Error(String(err));
1608
+ childRunStatus = "error";
1609
+ finalStats = { totalIn: 0, totalOut: 0, turns: 0, elapsed: 0 };
1610
+ result = `[sub-agent ${id}] Error: ${error.message}`;
1611
+ await ctx.hooks.callHook("spawn:error", { id, task, depth: childDepth, error });
1612
+ }
1613
+ } finally {
1614
+ try {
1615
+ await agent.destroy();
1616
+ } catch (err) {
1617
+ destroyError = err instanceof Error ? err : new Error(String(err));
1618
+ }
1619
+ }
1620
+ if (finalStats) {
1621
+ localStats.totalIn += finalStats.totalIn;
1622
+ localStats.totalOut += finalStats.totalOut;
1623
+ localStats.turns += finalStats.turns;
1624
+ localStats.elapsed += finalStats.elapsed;
1625
+ }
1626
+ const childRunStats = {
1399
1627
  id,
1400
1628
  task,
1401
- stats
1402
- });
1403
- const response = extractText(agent.turns.at(-1));
1404
- return [
1405
- `[sub-agent ${id}] Completed in ${stats.turns} turns (${stats.elapsed}ms)`,
1406
- `Tokens: ${stats.totalIn} in / ${stats.totalOut} out`,
1407
- "",
1408
- response || "(no text response)"
1409
- ].join("\n");
1410
- } catch (err) {
1411
- await ctx.hooks.callHook("spawn:error", { id, task, error: err });
1412
- return `[sub-agent ${id}] Error: ${err.message}`;
1629
+ stats: finalStats,
1630
+ depth: childDepth,
1631
+ status: childRunStatus,
1632
+ ...finalStats.output ? { output: finalStats.output } : {}
1633
+ };
1634
+ options.onComplete?.(child, finalStats, childRunStatus);
1635
+ await ctx.hooks.callHook("spawn:complete", childRunStats);
1636
+ if (destroyError) {
1637
+ await ctx.hooks.callHook("spawn:error", {
1638
+ id,
1639
+ task,
1640
+ depth: childDepth,
1641
+ error: destroyError
1642
+ });
1643
+ }
1644
+ return result;
1413
1645
  } finally {
1646
+ unbubble?.();
1414
1647
  localActiveCount--;
1415
- await agent.destroy();
1416
1648
  localChildren.delete(id);
1417
1649
  }
1418
1650
  }
@@ -14,11 +14,18 @@ var AgentProviderError = class extends Error {
14
14
  code = "provider_error";
15
15
  provider;
16
16
  providerCode;
17
+ /**
18
+ * Whether a retry with backoff is likely to succeed. See
19
+ * {@link ClassifiedError.retryable}. Absent when the provider did not
20
+ * classify the error — callers should treat absent as "don't retry".
21
+ */
22
+ retryable;
17
23
  constructor(message, options) {
18
24
  super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
19
25
  this.name = "AgentProviderError";
20
26
  this.provider = options.provider;
21
27
  this.providerCode = options.providerCode;
28
+ this.retryable = options.retryable;
22
29
  }
23
30
  };
24
31
  var AgentAbortedError = class extends Error {
@@ -74,6 +81,7 @@ function toTypedError(classification, provider, cause) {
74
81
  return new AgentProviderError(message, {
75
82
  provider,
76
83
  providerCode: classification.providerCode,
84
+ retryable: classification.retryable,
77
85
  cause
78
86
  });
79
87
  }
@@ -368,12 +368,14 @@ async function createSession(options = {}) {
368
368
  get metadata() {
369
369
  return data.metadata;
370
370
  },
371
- startRun(runId, prompt) {
371
+ startRun(runId, prompt, extras) {
372
372
  data.runs.push({
373
373
  id: runId,
374
374
  startedAt: Date.now(),
375
375
  prompt: prompt ?? "",
376
- status: "running"
376
+ status: "running",
377
+ ...extras?.parentRunId ? { parentRunId: extras.parentRunId } : {},
378
+ ...typeof extras?.depth === "number" ? { depth: extras.depth } : {}
377
379
  });
378
380
  touch();
379
381
  },