@cjhyy/code-shell 0.1.0-alpha.0 → 0.1.0-alpha.1

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/CHANGELOG.md CHANGED
@@ -8,6 +8,45 @@ breaking.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.1.0-alpha.1] - 2026-04-30
12
+
13
+ ### Changed (breaking)
14
+ - **Tool timeout system rewritten.** Removed the hardcoded
15
+ `LEGACY_LONG_TIMEOUT_TOOLS = {Agent, Arena}` whitelist in
16
+ `tool-system/registry.ts`. Tools now declare their own timeout via
17
+ `RegisteredTool.timeoutMs` at registration time. Precedence:
18
+ `executeTool(opts.timeoutMs)` > `tool.timeoutMs` > `DEFAULT_TOOL_TIMEOUT_MS`
19
+ (120s). Custom long-running tools registered via `engine.registerCustomTool`
20
+ can now set a higher timeout instead of being silently capped at 120s.
21
+ - **Bash internal 600s cap removed.** `bash.ts` no longer clamps the
22
+ user-supplied `timeout` argument. The outer registry caps via the tool's
23
+ declared `timeoutMs` (default 1h for Bash). Long-running shell loops
24
+ (`until cond; do ...; done`) are now feasible.
25
+ - **Agent `max_turns` upper bound (30) removed.** Sub-agents that need to do
26
+ more turns (deep research, large refactors) are no longer artificially
27
+ capped. Default remains 15.
28
+ - **Sub-agent LLM call timeout (60s) removed.** `agent.ts` no longer clamps
29
+ `subAgentConfig.llm.timeout` to 60s, which was breaking slow models
30
+ (e.g. extended thinking).
31
+
32
+ ### Added
33
+ - **`Agent(run_in_background: true)`** — fire-and-forget sub-agents. Returns
34
+ an `agent_id` immediately instead of blocking the parent turn. The agent
35
+ runs detached in the same process; restarting loses its state.
36
+ - **`AgentStatus(agent_id?)`** — query background agent state
37
+ (running / completed / failed / cancelled), or list all when `agent_id` is
38
+ omitted. Returns the result text once completed.
39
+ - **`AgentCancel(agent_id)`** — abort a running background agent.
40
+ - New module `src/tool-system/builtin/agent-registry.ts` — in-process
41
+ registry for async agent handles.
42
+
43
+ ### Notes
44
+ - Cross-process / restart-survivable long tasks still belong to `RunManager`
45
+ in `@cjhyy/code-shell/run`, not to `Agent(run_in_background)`. The split
46
+ mirrors Claude Code's REPL/Agent-tool/Routines architecture.
47
+
48
+ ## [0.1.0-alpha.0] - 2026-04-28
49
+
11
50
  ### Added
12
51
  - **`IterativeArena` — multi-model authoring loop.** Pipeline: tournament v1
13
52
  (every participant writes a draft, the author merges anonymized drafts into
@@ -4,7 +4,7 @@ import {
4
4
  NoopEvaluator,
5
5
  RunManager,
6
6
  registerPreset
7
- } from "./chunk-7RMAR2Y3.js";
7
+ } from "./chunk-JUHRV2XF.js";
8
8
  import {
9
9
  init_esm_shims
10
10
  } from "./chunk-DI7RDLOS.js";
@@ -90,11 +90,14 @@ function defineProduct(definition, runtime) {
90
90
  disabledBuiltinTools: adapter?.disableTools,
91
91
  customSystemPrompt: presetDef.customPrompt,
92
92
  appendSystemPrompt: presetDef.appendPrompt,
93
+ hooks: adapter?.hooks,
93
94
  customTools: customToolEntries.length > 0 ? customToolEntries : void 0
94
95
  },
95
96
  concurrency: contract?.concurrency ?? 1,
96
97
  runsDir,
97
- evaluator
98
+ evaluator,
99
+ defaultTags: contract?.defaultTags,
100
+ defaultMetadata: contract?.defaultMetadata
98
101
  });
99
102
  return { manager, preset: agentPreset, customTools };
100
103
  }
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  FileRunStore,
3
3
  RunManager
4
- } from "./chunk-7RMAR2Y3.js";
4
+ } from "./chunk-JUHRV2XF.js";
5
5
  import {
6
6
  init_esm_shims
7
7
  } from "./chunk-DI7RDLOS.js";
@@ -28,11 +28,14 @@ function createRunManager(options) {
28
28
  enabledBuiltinTools: options.enabledBuiltinTools,
29
29
  disabledBuiltinTools: options.disabledBuiltinTools,
30
30
  customSystemPrompt: options.customSystemPrompt,
31
- appendSystemPrompt: options.appendSystemPrompt
31
+ appendSystemPrompt: options.appendSystemPrompt,
32
+ hooks: options.hooks
32
33
  },
33
34
  concurrency: options.concurrency ?? 1,
34
35
  runsDir,
35
- evaluator: options.evaluator
36
+ evaluator: options.evaluator,
37
+ defaultTags: options.defaultTags,
38
+ defaultMetadata: options.defaultMetadata
36
39
  });
37
40
  }
38
41
 
@@ -814,7 +814,7 @@ var bashToolDef = {
814
814
  command: { type: "string", description: "The shell command to execute" },
815
815
  timeout: {
816
816
  type: "number",
817
- description: "Timeout in milliseconds (default: 120000, max: 600000)"
817
+ description: "Timeout in milliseconds (default: 120000). Outer registry caps at 1h."
818
818
  },
819
819
  description: {
820
820
  type: "string",
@@ -828,7 +828,7 @@ var MAX_OUTPUT = 1e5;
828
828
  async function bashTool(args) {
829
829
  const command = args.command;
830
830
  if (!command) return "Error: command is required";
831
- const timeout = Math.min(args.timeout || 12e4, 6e5);
831
+ const timeout = args.timeout || 12e4;
832
832
  try {
833
833
  const { stdout, stderr } = await execAsync(command, {
834
834
  timeout,
@@ -903,6 +903,63 @@ async function askUserTool(args) {
903
903
  // src/tool-system/builtin/agent.ts
904
904
  init_esm_shims();
905
905
  init_plan();
906
+
907
+ // src/tool-system/builtin/agent-registry.ts
908
+ init_esm_shims();
909
+ var AsyncAgentRegistry = class {
910
+ agents = /* @__PURE__ */ new Map();
911
+ register(entry) {
912
+ this.agents.set(entry.agentId, entry);
913
+ }
914
+ get(agentId) {
915
+ return this.agents.get(agentId);
916
+ }
917
+ list() {
918
+ return [...this.agents.values()];
919
+ }
920
+ markCompleted(agentId, result) {
921
+ const e = this.agents.get(agentId);
922
+ if (!e) return;
923
+ if (e.status !== "running") return;
924
+ e.status = "completed";
925
+ e.result = result;
926
+ e.finishedAt = Date.now();
927
+ }
928
+ markFailed(agentId, error) {
929
+ const e = this.agents.get(agentId);
930
+ if (!e) return;
931
+ if (e.status !== "running") return;
932
+ e.status = "failed";
933
+ e.error = error;
934
+ e.finishedAt = Date.now();
935
+ }
936
+ cancel(agentId) {
937
+ const e = this.agents.get(agentId);
938
+ if (!e) return false;
939
+ if (e.status !== "running") return false;
940
+ try {
941
+ e.abort();
942
+ } catch {
943
+ }
944
+ e.status = "cancelled";
945
+ e.finishedAt = Date.now();
946
+ return true;
947
+ }
948
+ reset() {
949
+ for (const e of this.agents.values()) {
950
+ if (e.status === "running") {
951
+ try {
952
+ e.abort();
953
+ } catch {
954
+ }
955
+ }
956
+ }
957
+ this.agents.clear();
958
+ }
959
+ };
960
+ var asyncAgentRegistry = new AsyncAgentRegistry();
961
+
962
+ // src/tool-system/builtin/agent.ts
906
963
  import { nanoid } from "nanoid";
907
964
  var agentToolDef = {
908
965
  name: "Agent",
@@ -921,6 +978,10 @@ var agentToolDef = {
921
978
  max_turns: {
922
979
  type: "number",
923
980
  description: "Maximum turns for the sub-agent (default: 15)"
981
+ },
982
+ run_in_background: {
983
+ type: "boolean",
984
+ description: "If true, launch the sub-agent in the background and return an agent_id immediately instead of waiting for it to finish. Use AgentStatus(agent_id) to check progress and AgentCancel(agent_id) to stop it. The agent runs in this process; restarting loses its state. Default: false (synchronous wait)."
924
985
  }
925
986
  },
926
987
  required: ["description", "prompt"]
@@ -930,35 +991,21 @@ var _subAgentConfig;
930
991
  function setSubAgentConfig(config) {
931
992
  _subAgentConfig = config;
932
993
  }
933
- async function agentTool(args) {
934
- const prompt = args.prompt;
935
- const description = args.description || "sub-agent";
936
- if (!prompt) return "Error: prompt is required";
937
- if (!_subAgentConfig) {
938
- return "Error: Agent tool is not configured.";
939
- }
940
- const signal = args.__signal;
941
- if (signal?.aborted) {
942
- return "Agent aborted before starting.";
943
- }
944
- const maxTurns = Math.min(args.max_turns || 15, 30);
945
- const agentId = nanoid(8);
946
- const parentStream = _subAgentConfig.onStream;
994
+ async function runSubAgent(opts) {
995
+ if (!_subAgentConfig) throw new Error("Agent tool is not configured.");
996
+ const { agentId, description, prompt, maxTurns, signal, parentStream } = opts;
947
997
  parentStream?.({ type: "agent_start", agentId, description });
948
998
  const childStream = (event) => {
949
999
  if (!parentStream) return;
950
1000
  const tagged = { ...event, agentId };
951
1001
  parentStream(tagged);
952
1002
  };
1003
+ const parentWasInPlanMode = isInPlanMode();
1004
+ if (parentWasInPlanMode) resetPlanMode();
953
1005
  try {
954
- const parentWasInPlanMode = isInPlanMode();
955
- if (parentWasInPlanMode) {
956
- resetPlanMode();
957
- }
958
1006
  const engine = _subAgentConfig.createEngine({
959
1007
  llm: {
960
1008
  ..._subAgentConfig.llm,
961
- timeout: Math.min(_subAgentConfig.llm.timeout ?? 12e4, 6e4),
962
1009
  retryMaxAttempts: 2
963
1010
  },
964
1011
  cwd: _subAgentConfig.cwd,
@@ -972,27 +1019,142 @@ async function agentTool(args) {
972
1019
  maxContextTokens: _subAgentConfig.maxContextTokens,
973
1020
  sessionStorageDir: _subAgentConfig.sessionStorageDir
974
1021
  });
975
- let result;
976
- try {
977
- result = await engine.run(prompt, { signal, onStream: childStream });
978
- } finally {
979
- if (parentWasInPlanMode) {
980
- restorePlanMode();
981
- }
982
- }
1022
+ const result = await engine.run(prompt, { signal, onStream: childStream });
983
1023
  parentStream?.({ type: "agent_end", agentId, description });
984
- if (result.text) {
985
- return result.text;
986
- }
987
- return `Agent completed (${result.reason}) but produced no text output.`;
1024
+ return result.text || `Agent completed (${result.reason}) but produced no text output.`;
1025
+ } finally {
1026
+ if (parentWasInPlanMode) restorePlanMode();
1027
+ }
1028
+ }
1029
+ async function agentTool(args) {
1030
+ const prompt = args.prompt;
1031
+ const description = args.description || "sub-agent";
1032
+ if (!prompt) return "Error: prompt is required";
1033
+ if (!_subAgentConfig) {
1034
+ return "Error: Agent tool is not configured.";
1035
+ }
1036
+ const parentSignal = args.__signal;
1037
+ if (parentSignal?.aborted) {
1038
+ return "Agent aborted before starting.";
1039
+ }
1040
+ const maxTurns = args.max_turns || 15;
1041
+ const runInBackground = args.run_in_background === true;
1042
+ const agentId = nanoid(8);
1043
+ const parentStream = _subAgentConfig.onStream;
1044
+ if (runInBackground) {
1045
+ const controller = new AbortController();
1046
+ asyncAgentRegistry.register({
1047
+ agentId,
1048
+ description,
1049
+ status: "running",
1050
+ startedAt: Date.now(),
1051
+ abort: () => controller.abort()
1052
+ });
1053
+ void runSubAgent({
1054
+ agentId,
1055
+ description,
1056
+ prompt,
1057
+ maxTurns,
1058
+ signal: controller.signal,
1059
+ parentStream
1060
+ }).then((text) => asyncAgentRegistry.markCompleted(agentId, text)).catch((err) => {
1061
+ if (controller.signal.aborted) {
1062
+ return;
1063
+ }
1064
+ asyncAgentRegistry.markFailed(agentId, err.message);
1065
+ });
1066
+ return [
1067
+ `Agent launched in background.`,
1068
+ `agent_id: ${agentId}`,
1069
+ `description: ${description}`,
1070
+ ``,
1071
+ `Use AgentStatus(agent_id="${agentId}") to check progress or fetch the result.`,
1072
+ `Use AgentCancel(agent_id="${agentId}") to stop it.`
1073
+ ].join("\n");
1074
+ }
1075
+ try {
1076
+ return await runSubAgent({
1077
+ agentId,
1078
+ description,
1079
+ prompt,
1080
+ maxTurns,
1081
+ signal: parentSignal ?? new AbortController().signal,
1082
+ parentStream
1083
+ });
988
1084
  } catch (err) {
989
1085
  parentStream?.({ type: "agent_end", agentId, description, error: err.message });
990
- if (signal?.aborted) {
1086
+ if (parentSignal?.aborted) {
991
1087
  return "Agent was aborted.";
992
1088
  }
993
1089
  return `Agent error: ${err.message}`;
994
1090
  }
995
1091
  }
1092
+ var agentStatusToolDef = {
1093
+ name: "AgentStatus",
1094
+ description: "Check the status of a background agent launched with Agent(run_in_background=true). Returns running / completed / failed / cancelled, plus the result text once finished. Omit agent_id to list all background agents in this process.",
1095
+ inputSchema: {
1096
+ type: "object",
1097
+ properties: {
1098
+ agent_id: {
1099
+ type: "string",
1100
+ description: "The agent_id returned by Agent(run_in_background=true). Omit to list all."
1101
+ }
1102
+ }
1103
+ }
1104
+ };
1105
+ async function agentStatusTool(args) {
1106
+ const agentId = args.agent_id;
1107
+ if (!agentId) {
1108
+ const all = asyncAgentRegistry.list();
1109
+ if (all.length === 0) return "No background agents in this process.";
1110
+ return all.map((e2) => {
1111
+ const dur2 = ((e2.finishedAt ?? Date.now()) - e2.startedAt) / 1e3;
1112
+ return `${e2.agentId} [${e2.status}] ${e2.description} (${dur2.toFixed(1)}s)`;
1113
+ }).join("\n");
1114
+ }
1115
+ const e = asyncAgentRegistry.get(agentId);
1116
+ if (!e) return `Error: agent_id "${agentId}" not found.`;
1117
+ const dur = ((e.finishedAt ?? Date.now()) - e.startedAt) / 1e3;
1118
+ const lines = [
1119
+ `agent_id: ${e.agentId}`,
1120
+ `status: ${e.status}`,
1121
+ `description: ${e.description}`,
1122
+ `duration: ${dur.toFixed(1)}s`
1123
+ ];
1124
+ if (e.status === "completed" && e.result) {
1125
+ lines.push("", "\u2500\u2500 result \u2500\u2500", e.result);
1126
+ } else if (e.status === "failed" && e.error) {
1127
+ lines.push("", "\u2500\u2500 error \u2500\u2500", e.error);
1128
+ } else if (e.status === "running") {
1129
+ lines.push("", "(still running \u2014 call AgentStatus again later)");
1130
+ }
1131
+ return lines.join("\n");
1132
+ }
1133
+ var agentCancelToolDef = {
1134
+ name: "AgentCancel",
1135
+ description: "Cancel a background agent launched with Agent(run_in_background=true). The agent's current LLM call and any in-flight tools will be aborted.",
1136
+ inputSchema: {
1137
+ type: "object",
1138
+ properties: {
1139
+ agent_id: {
1140
+ type: "string",
1141
+ description: "The agent_id to cancel."
1142
+ }
1143
+ },
1144
+ required: ["agent_id"]
1145
+ }
1146
+ };
1147
+ async function agentCancelTool(args) {
1148
+ const agentId = args.agent_id;
1149
+ if (!agentId) return "Error: agent_id is required.";
1150
+ const e = asyncAgentRegistry.get(agentId);
1151
+ if (!e) return `Error: agent_id "${agentId}" not found.`;
1152
+ if (e.status !== "running") {
1153
+ return `Agent ${agentId} is already ${e.status}; nothing to cancel.`;
1154
+ }
1155
+ const ok = asyncAgentRegistry.cancel(agentId);
1156
+ return ok ? `Agent ${agentId} cancelled.` : `Failed to cancel agent ${agentId}.`;
1157
+ }
996
1158
 
997
1159
  // src/tool-system/builtin/index.ts
998
1160
  init_plan();
@@ -2321,7 +2483,9 @@ var BUILTIN_TOOLS = [
2321
2483
  source: "builtin",
2322
2484
  permissionDefault: "ask",
2323
2485
  isReadOnly: false,
2324
- isConcurrencySafe: false
2486
+ isConcurrencySafe: false,
2487
+ timeoutMs: 36e5
2488
+ // 1h — supports long-running shell loops (e.g. `until` polling)
2325
2489
  },
2326
2490
  execute: bashTool
2327
2491
  },
@@ -2361,10 +2525,32 @@ var BUILTIN_TOOLS = [
2361
2525
  source: "builtin",
2362
2526
  permissionDefault: "allow",
2363
2527
  isReadOnly: true,
2364
- isConcurrencySafe: true
2528
+ isConcurrencySafe: true,
2529
+ timeoutMs: 18e5
2530
+ // 30min — sub-agent runs may execute many tool calls
2365
2531
  },
2366
2532
  execute: agentTool
2367
2533
  },
2534
+ {
2535
+ definition: {
2536
+ ...agentStatusToolDef,
2537
+ source: "builtin",
2538
+ permissionDefault: "allow",
2539
+ isReadOnly: true,
2540
+ isConcurrencySafe: true
2541
+ },
2542
+ execute: agentStatusTool
2543
+ },
2544
+ {
2545
+ definition: {
2546
+ ...agentCancelToolDef,
2547
+ source: "builtin",
2548
+ permissionDefault: "allow",
2549
+ isReadOnly: false,
2550
+ isConcurrencySafe: false
2551
+ },
2552
+ execute: agentCancelTool
2553
+ },
2368
2554
  {
2369
2555
  definition: {
2370
2556
  ...enterPlanModeToolDef,
@@ -2647,7 +2833,9 @@ var BUILTIN_TOOLS = [
2647
2833
  source: "builtin",
2648
2834
  permissionDefault: "ask",
2649
2835
  isReadOnly: true,
2650
- isConcurrencySafe: false
2836
+ isConcurrencySafe: false,
2837
+ timeoutMs: 18e5
2838
+ // 30min — multi-model debate rounds take time
2651
2839
  },
2652
2840
  execute: arenaTool
2653
2841
  }
@@ -2655,6 +2843,7 @@ var BUILTIN_TOOLS = [
2655
2843
 
2656
2844
  // src/tool-system/registry.ts
2657
2845
  init_esm_shims();
2846
+ var DEFAULT_TOOL_TIMEOUT_MS = 12e4;
2658
2847
  var ToolRegistry = class {
2659
2848
  tools = /* @__PURE__ */ new Map();
2660
2849
  builtinExecutors = /* @__PURE__ */ new Map();
@@ -2709,10 +2898,7 @@ var ToolRegistry = class {
2709
2898
  if (!executor) {
2710
2899
  throw new ToolExecutionError(name, "No executor registered for this tool");
2711
2900
  }
2712
- const LONG_TIMEOUT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Arena"]);
2713
- const isLongRunning = LONG_TIMEOUT_TOOLS.has(name);
2714
- const defaultTimeout = isLongRunning ? 18e5 : 12e4;
2715
- const timeout = options?.timeoutMs ?? defaultTimeout;
2901
+ const timeout = options?.timeoutMs ?? tool.timeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS;
2716
2902
  const parentSignal = options?.signal;
2717
2903
  const id = `tool_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
2718
2904
  if (parentSignal?.aborted) {
@@ -3126,7 +3312,7 @@ var InteractiveApprovalBackend = class {
3126
3312
  if (toolRule === "allow") return { approved: true };
3127
3313
  if (toolRule === "deny") return { approved: false };
3128
3314
  if (!this.promptFn) {
3129
- return { approved: true };
3315
+ return { approved: false, reason: "interactive approval backend has no prompt function" };
3130
3316
  }
3131
3317
  const result = await this.promptFn(req);
3132
3318
  if (result.always && result.approved) {
@@ -3870,6 +4056,8 @@ var GENERAL_BUILTIN_TOOLS = [
3870
4056
  "WebFetch",
3871
4057
  "AskUserQuestion",
3872
4058
  "Agent",
4059
+ "AgentStatus",
4060
+ "AgentCancel",
3873
4061
  "EnterPlanMode",
3874
4062
  "ExitPlanMode",
3875
4063
  "ToolSearch",
@@ -3909,6 +4097,8 @@ var GENERAL_PERMISSION_RULES = [
3909
4097
  { tool: "WebFetch", decision: "allow" },
3910
4098
  { tool: "AskUserQuestion", decision: "allow" },
3911
4099
  { tool: "Agent", decision: "allow" },
4100
+ { tool: "AgentStatus", decision: "allow" },
4101
+ { tool: "AgentCancel", decision: "allow" },
3912
4102
  { tool: "EnterPlanMode", decision: "allow" },
3913
4103
  { tool: "ExitPlanMode", decision: "allow" },
3914
4104
  { tool: "ToolSearch", decision: "allow" },
@@ -4433,19 +4623,19 @@ var TurnLoop = class {
4433
4623
  } catch (retryErr) {
4434
4624
  if (!(retryErr instanceof ContextLimitError)) {
4435
4625
  this.config.onStream?.({ type: "error", error: retryErr.message });
4436
- return { text: finalText, reason: "model_error" };
4626
+ return { text: finalText, reason: "model_error", messages };
4437
4627
  }
4438
4628
  }
4439
4629
  }
4440
4630
  if (!recovered) {
4441
4631
  this.patchOrphanedToolUses(messages);
4442
4632
  this.config.onStream?.({ type: "error", error: "Context limit exceeded after 3 recovery attempts" });
4443
- return { text: finalText, reason: "prompt_too_long" };
4633
+ return { text: finalText, reason: "prompt_too_long", messages };
4444
4634
  }
4445
4635
  } else {
4446
4636
  this.patchOrphanedToolUses(messages);
4447
4637
  this.config.onStream?.({ type: "error", error: err.message });
4448
- return { text: finalText, reason: "model_error" };
4638
+ return { text: finalText, reason: "model_error", messages };
4449
4639
  }
4450
4640
  }
4451
4641
  if (response.usage?.promptTokens !== void 0) {
@@ -4483,7 +4673,7 @@ var TurnLoop = class {
4483
4673
  response = { ...response, text: combinedText };
4484
4674
  }
4485
4675
  if (this.config.signal?.aborted) {
4486
- return { text: finalText, reason: "aborted_streaming" };
4676
+ return { text: finalText, reason: "aborted_streaming", messages };
4487
4677
  }
4488
4678
  if (response.text) {
4489
4679
  finalText = response.text;
@@ -4502,7 +4692,8 @@ var TurnLoop = class {
4502
4692
  turnNumber: this.turnCount,
4503
4693
  hasToolUse: false
4504
4694
  });
4505
- return { text: finalText, reason: "completed" };
4695
+ messages.push({ role: "assistant", content: finalText });
4696
+ return { text: finalText, reason: "completed", messages };
4506
4697
  }
4507
4698
  logger.info("turn.tool_use", { turn: this.turnCount, tools: response.toolCalls.map((t) => t.toolName) });
4508
4699
  const toolCalls = response.toolCalls.slice(0, this.config.maxToolCallsPerTurn);
@@ -4566,7 +4757,8 @@ var TurnLoop = class {
4566
4757
  type: "assistant_message",
4567
4758
  message: { role: "assistant", content: finalText }
4568
4759
  });
4569
- return { text: finalText, reason: "completed" };
4760
+ messages.push({ role: "assistant", content: finalText });
4761
+ return { text: finalText, reason: "completed", messages };
4570
4762
  }
4571
4763
  if (budgetDecision === "nudge") {
4572
4764
  messages.push({
@@ -4607,9 +4799,10 @@ var TurnLoop = class {
4607
4799
  type: "assistant_message",
4608
4800
  message: { role: "assistant", content: finalText }
4609
4801
  });
4802
+ messages.push({ role: "assistant", content: finalText });
4610
4803
  }
4611
4804
  this.config.onStream?.({ type: "turn_complete", reason: "max_turns" });
4612
- return { text: finalText, reason: "max_turns" };
4805
+ return { text: finalText, reason: "max_turns", messages };
4613
4806
  }
4614
4807
  /**
4615
4808
  * Call model with streaming fallback.
@@ -4750,6 +4943,9 @@ var Engine = class _Engine {
4750
4943
  })
4751
4944
  });
4752
4945
  this.hooks = new HookRegistry();
4946
+ for (const hook of config.hooks ?? []) {
4947
+ this.hooks.register(hook.event, hook.handler, hook.priority, hook.name);
4948
+ }
4753
4949
  this.sessionManager = new SessionManager(config.sessionStorageDir);
4754
4950
  this.modelPool = new ModelPool();
4755
4951
  try {
@@ -4819,7 +5015,7 @@ var Engine = class _Engine {
4819
5015
  let messages;
4820
5016
  if (options?.sessionId) {
4821
5017
  session = this.sessionManager.resume(options.sessionId);
4822
- messages = session.transcript.toMessages();
5018
+ messages = this.compactedMessagesBySession.get(options.sessionId) ? [...this.compactedMessagesBySession.get(options.sessionId)] : session.transcript.toMessages();
4823
5019
  if (session.state.costState && this.config.costStore) {
4824
5020
  this.config.costStore.restore(session.state.costState);
4825
5021
  }
@@ -4860,7 +5056,7 @@ var Engine = class _Engine {
4860
5056
  approvalBackend = new AutoApprovalBackend();
4861
5057
  } else {
4862
5058
  approvalBackend = new HeadlessApprovalBackend(
4863
- mode === "bypassPermissions" ? "approve-all" : mode === "dontAsk" ? "deny-all" : "approve-all"
5059
+ mode === "bypassPermissions" ? "approve-all" : mode === "dontAsk" ? "deny-all" : "deny-all"
4864
5060
  );
4865
5061
  }
4866
5062
  const permission = new PermissionClassifier(defaultRules, mode, approvalBackend);
@@ -4912,6 +5108,8 @@ var Engine = class _Engine {
4912
5108
  if (userContextMsg) {
4913
5109
  messages.unshift(userContextMsg);
4914
5110
  }
5111
+ this.lastSessionId = session.state.sessionId;
5112
+ this.lastMessages = messages;
4915
5113
  contextManager.setTranscriptPath(session.transcript.getFilePath());
4916
5114
  contextManager.setSummarizeFn(async (prompt) => {
4917
5115
  const summaryResponse = await llmClient.createMessage({
@@ -4977,6 +5175,11 @@ var Engine = class _Engine {
4977
5175
  }
4978
5176
  );
4979
5177
  const result = await turnLoop.run(messages);
5178
+ this.lastMessages = result.messages;
5179
+ this.compactedMessagesBySession.set(
5180
+ session.state.sessionId,
5181
+ this.stripUserContextMessage(result.messages, userContextMsg)
5182
+ );
4980
5183
  logger.info("engine.done", {
4981
5184
  sessionId: session.state.sessionId,
4982
5185
  reason: result.reason,
@@ -5059,19 +5262,29 @@ var Engine = class _Engine {
5059
5262
  * Returns token stats before/after.
5060
5263
  */
5061
5264
  forceCompact() {
5062
- if (!this.lastContextManager || !this.lastMessages) {
5265
+ const sessionId = this.lastSessionId;
5266
+ if (!this.lastContextManager || !sessionId) {
5063
5267
  return { before: 0, after: 0, strategy: "none (no active session)" };
5064
5268
  }
5065
5269
  const { estimateTokens: estimateTokens2 } = (init_compaction(), __toCommonJS(compaction_exports));
5066
- const before = estimateTokens2(this.lastMessages);
5067
- this.lastMessages = this.lastContextManager.manage(this.lastMessages);
5068
- const after = estimateTokens2(this.lastMessages);
5270
+ const sourceMessages = this.compactedMessagesBySession.get(sessionId) ?? this.sessionManager.resume(sessionId).transcript.toMessages();
5271
+ const before = estimateTokens2(sourceMessages);
5272
+ const compacted = this.lastContextManager.manage(sourceMessages);
5273
+ const after = estimateTokens2(compacted);
5274
+ this.compactedMessagesBySession.set(sessionId, compacted);
5275
+ this.lastMessages = compacted;
5069
5276
  return {
5070
5277
  before,
5071
5278
  after,
5072
5279
  strategy: before === after ? "no compaction needed" : "compacted"
5073
5280
  };
5074
5281
  }
5282
+ stripUserContextMessage(messages, userContextMsg) {
5283
+ if (!userContextMsg || messages[0] !== userContextMsg) {
5284
+ return [...messages];
5285
+ }
5286
+ return messages.slice(1);
5287
+ }
5075
5288
  /**
5076
5289
  * Update a config setting at runtime.
5077
5290
  */
@@ -5090,6 +5303,8 @@ var Engine = class _Engine {
5090
5303
  /** Track last context manager and messages for /compact support. */
5091
5304
  lastContextManager;
5092
5305
  lastMessages;
5306
+ lastSessionId;
5307
+ compactedMessagesBySession = /* @__PURE__ */ new Map();
5093
5308
  };
5094
5309
 
5095
5310
  // src/run/types.ts
@@ -5921,6 +6136,7 @@ var EngineRunner = class {
5921
6136
  appendSystemPrompt: this.config.appendSystemPrompt,
5922
6137
  sessionStorageDir: this.config.sessionStorageDir,
5923
6138
  mcpServers: this.config.mcpServers,
6139
+ hooks: this.config.hooks,
5924
6140
  approvalBackend,
5925
6141
  askUser: askUserFn,
5926
6142
  ...context.engineConfigOverrides
@@ -5961,6 +6177,8 @@ var RunManager = class {
5961
6177
  lock;
5962
6178
  heartbeat;
5963
6179
  evaluator;
6180
+ defaultTags;
6181
+ defaultMetadata;
5964
6182
  subscribers = /* @__PURE__ */ new Map();
5965
6183
  abortControllers = /* @__PURE__ */ new Map();
5966
6184
  /** Active execution handles — used to resolve pending approvals/input while Engine is suspended */
@@ -5978,6 +6196,8 @@ var RunManager = class {
5978
6196
  intervalMs: config.heartbeatIntervalMs
5979
6197
  });
5980
6198
  this.evaluator = config.evaluator ?? new NoopEvaluator();
6199
+ this.defaultTags = config.defaultTags ?? [];
6200
+ this.defaultMetadata = config.defaultMetadata ?? {};
5981
6201
  this.queue.setExecutor((runId) => this.executeRun(runId));
5982
6202
  }
5983
6203
  // ─── Submit ────────────────────────────────────────────────────
@@ -6002,8 +6222,8 @@ var RunManager = class {
6002
6222
  latestApprovalId: null,
6003
6223
  summary: null,
6004
6224
  error: null,
6005
- tags: input.tags ?? [],
6006
- metadata: input.metadata ?? {}
6225
+ tags: [.../* @__PURE__ */ new Set([...this.defaultTags, ...input.tags ?? []])],
6226
+ metadata: { ...this.defaultMetadata, ...input.metadata ?? {} }
6007
6227
  };
6008
6228
  await this.store.create(snapshot);
6009
6229
  await this.emitRunEvent(runId, "run_created", { objective: input.objective });
@@ -6471,6 +6691,7 @@ export {
6471
6691
  ToolExecutor,
6472
6692
  HeadlessApprovalBackend,
6473
6693
  AutoApprovalBackend,
6694
+ getInteractiveApprovalBackend,
6474
6695
  setInteractiveApprovalFn,
6475
6696
  setRuntimeBypass,
6476
6697
  PermissionClassifier,