@oh-my-pi/pi-coding-agent 15.7.2 → 15.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/CHANGELOG.md +114 -6
  2. package/dist/types/cli/args.d.ts +1 -1
  3. package/dist/types/cli/extension-flags.d.ts +36 -0
  4. package/dist/types/config/config-file.d.ts +4 -0
  5. package/dist/types/config/file-lock.d.ts +23 -0
  6. package/dist/types/config/keybindings.d.ts +2 -1
  7. package/dist/types/config/model-registry.d.ts +6 -0
  8. package/dist/types/config/settings-schema.d.ts +69 -65
  9. package/dist/types/edit/hashline/diff.d.ts +3 -3
  10. package/dist/types/eval/__tests__/agent-bridge.test.d.ts +1 -0
  11. package/dist/types/eval/__tests__/budget-bridge.test.d.ts +1 -0
  12. package/dist/types/eval/__tests__/idle-timeout.test.d.ts +1 -0
  13. package/dist/types/eval/agent-bridge.d.ts +25 -0
  14. package/dist/types/eval/backend.d.ts +17 -2
  15. package/dist/types/eval/budget-bridge.d.ts +29 -0
  16. package/dist/types/eval/idle-timeout.d.ts +28 -0
  17. package/dist/types/eval/js/executor.d.ts +8 -0
  18. package/dist/types/eval/js/tool-bridge.d.ts +2 -1
  19. package/dist/types/eval/py/executor.d.ts +13 -0
  20. package/dist/types/exec/bash-executor.d.ts +1 -0
  21. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  22. package/dist/types/extensibility/extensions/runner.d.ts +7 -0
  23. package/dist/types/extensibility/plugins/git-url.d.ts +11 -1
  24. package/dist/types/extensibility/plugins/manager.d.ts +12 -1
  25. package/dist/types/extensibility/shared-events.d.ts +2 -2
  26. package/dist/types/internal-urls/local-protocol.d.ts +19 -9
  27. package/dist/types/internal-urls/types.d.ts +14 -0
  28. package/dist/types/lsp/client.d.ts +3 -0
  29. package/dist/types/mcp/manager.d.ts +14 -5
  30. package/dist/types/memory-backend/index.d.ts +1 -1
  31. package/dist/types/memory-backend/resolve.d.ts +1 -1
  32. package/dist/types/memory-backend/types.d.ts +3 -3
  33. package/dist/types/mnemopi/backend.d.ts +4 -0
  34. package/dist/types/mnemopi/config.d.ts +29 -0
  35. package/dist/types/mnemopi/state.d.ts +72 -0
  36. package/dist/types/modes/components/custom-editor.d.ts +2 -2
  37. package/dist/types/modes/components/omfg-panel.d.ts +19 -0
  38. package/dist/types/modes/controllers/command-controller.d.ts +6 -0
  39. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  40. package/dist/types/modes/controllers/omfg-controller.d.ts +10 -0
  41. package/dist/types/modes/controllers/omfg-rule.d.ts +26 -0
  42. package/dist/types/modes/gradient-highlight.d.ts +5 -1
  43. package/dist/types/modes/interactive-mode.d.ts +7 -3
  44. package/dist/types/modes/magic-keywords.d.ts +14 -0
  45. package/dist/types/modes/markdown-prose.d.ts +27 -0
  46. package/dist/types/modes/orchestrate.d.ts +7 -2
  47. package/dist/types/modes/shared.d.ts +1 -1
  48. package/dist/types/modes/turn-budget.d.ts +18 -0
  49. package/dist/types/modes/types.d.ts +7 -3
  50. package/dist/types/modes/ultrathink.d.ts +7 -2
  51. package/dist/types/modes/workflow.d.ts +15 -0
  52. package/dist/types/sdk.d.ts +13 -3
  53. package/dist/types/session/agent-session.d.ts +36 -17
  54. package/dist/types/session/session-manager.d.ts +18 -0
  55. package/dist/types/session/session-storage.d.ts +6 -0
  56. package/dist/types/session/shake-types.d.ts +24 -0
  57. package/dist/types/task/executor.d.ts +2 -2
  58. package/dist/types/task/repair-args.d.ts +52 -0
  59. package/dist/types/tiny/models.d.ts +1 -1
  60. package/dist/types/tiny/title-client.d.ts +28 -2
  61. package/dist/types/tiny/title-protocol.d.ts +8 -5
  62. package/dist/types/tools/find.d.ts +1 -1
  63. package/dist/types/tools/index.d.ts +19 -3
  64. package/dist/types/tools/memory-edit.d.ts +1 -1
  65. package/dist/types/tools/path-utils.d.ts +7 -0
  66. package/dist/types/tui/output-block.d.ts +7 -7
  67. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  68. package/package.json +10 -10
  69. package/scripts/build-binary.ts +0 -1
  70. package/src/autoresearch/tools/run-experiment.ts +45 -113
  71. package/src/cli/args.ts +39 -16
  72. package/src/cli/extension-flags.ts +48 -0
  73. package/src/cli/plugin-cli.ts +11 -2
  74. package/src/cli.ts +59 -0
  75. package/src/config/config-file.ts +98 -13
  76. package/src/config/file-lock.ts +60 -17
  77. package/src/config/keybindings.ts +78 -27
  78. package/src/config/model-registry.ts +7 -1
  79. package/src/config/settings-schema.ts +73 -67
  80. package/src/config/settings.ts +22 -0
  81. package/src/edit/hashline/diff.ts +81 -24
  82. package/src/edit/renderer.ts +16 -12
  83. package/src/eval/__tests__/agent-bridge.test.ts +433 -0
  84. package/src/eval/__tests__/budget-bridge.test.ts +69 -0
  85. package/src/eval/__tests__/idle-timeout.test.ts +66 -0
  86. package/src/eval/__tests__/shared-executors.test.ts +21 -0
  87. package/src/eval/agent-bridge.ts +295 -0
  88. package/src/eval/backend.ts +17 -2
  89. package/src/eval/budget-bridge.ts +48 -0
  90. package/src/eval/idle-timeout.ts +80 -0
  91. package/src/eval/js/executor.ts +35 -7
  92. package/src/eval/js/index.ts +2 -1
  93. package/src/eval/js/shared/prelude.txt +85 -1
  94. package/src/eval/js/tool-bridge.ts +9 -0
  95. package/src/eval/py/executor.ts +41 -14
  96. package/src/eval/py/index.ts +2 -1
  97. package/src/eval/py/prelude.py +132 -1
  98. package/src/exec/bash-executor.ts +2 -3
  99. package/src/extensibility/custom-tools/types.ts +2 -2
  100. package/src/extensibility/extensions/runner.ts +12 -2
  101. package/src/extensibility/plugins/git-url.ts +90 -4
  102. package/src/extensibility/plugins/manager.ts +103 -7
  103. package/src/extensibility/shared-events.ts +2 -2
  104. package/src/internal-urls/docs-index.generated.ts +88 -88
  105. package/src/internal-urls/local-protocol.ts +23 -11
  106. package/src/internal-urls/types.ts +15 -0
  107. package/src/lsp/client.ts +28 -5
  108. package/src/main.ts +44 -55
  109. package/src/mcp/manager.ts +87 -4
  110. package/src/memory-backend/index.ts +1 -1
  111. package/src/memory-backend/resolve.ts +3 -3
  112. package/src/memory-backend/types.ts +3 -3
  113. package/src/{mnemosyne → mnemopi}/backend.ts +70 -70
  114. package/src/{mnemosyne → mnemopi}/config.ts +36 -36
  115. package/src/{mnemosyne → mnemopi}/state.ts +67 -67
  116. package/src/modes/components/agent-dashboard.ts +6 -6
  117. package/src/modes/components/custom-editor.ts +4 -11
  118. package/src/modes/components/extensions/state-manager.ts +3 -4
  119. package/src/modes/components/footer.ts +8 -9
  120. package/src/modes/components/hook-selector.ts +86 -20
  121. package/src/modes/components/oauth-selector.ts +93 -21
  122. package/src/modes/components/omfg-panel.ts +141 -0
  123. package/src/modes/components/settings-defs.ts +2 -2
  124. package/src/modes/components/tips.txt +2 -1
  125. package/src/modes/components/tool-execution.ts +38 -19
  126. package/src/modes/components/tree-selector.ts +4 -3
  127. package/src/modes/components/user-message-selector.ts +94 -19
  128. package/src/modes/components/user-message.ts +8 -1
  129. package/src/modes/controllers/command-controller.ts +25 -0
  130. package/src/modes/controllers/event-controller.ts +68 -3
  131. package/src/modes/controllers/input-controller.ts +14 -11
  132. package/src/modes/controllers/mcp-command-controller.ts +1 -1
  133. package/src/modes/controllers/omfg-controller.ts +283 -0
  134. package/src/modes/controllers/omfg-rule.ts +647 -0
  135. package/src/modes/controllers/selector-controller.ts +1 -0
  136. package/src/modes/gradient-highlight.ts +23 -6
  137. package/src/modes/interactive-mode.ts +41 -7
  138. package/src/modes/magic-keywords.ts +20 -0
  139. package/src/modes/markdown-prose.ts +247 -0
  140. package/src/modes/orchestrate.ts +17 -11
  141. package/src/modes/shared.ts +3 -11
  142. package/src/modes/turn-budget.ts +31 -0
  143. package/src/modes/types.ts +7 -1
  144. package/src/modes/ultrathink.ts +16 -10
  145. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  146. package/src/modes/workflow.ts +42 -0
  147. package/src/prompts/system/omfg-user.md +51 -0
  148. package/src/prompts/system/project-prompt.md +3 -2
  149. package/src/prompts/system/subagent-system-prompt.md +12 -8
  150. package/src/prompts/system/system-prompt.md +9 -6
  151. package/src/prompts/system/workflow-notice.md +70 -0
  152. package/src/prompts/tools/eval.md +13 -1
  153. package/src/prompts/tools/memory-edit.md +1 -1
  154. package/src/sdk.ts +63 -33
  155. package/src/session/agent-session.ts +290 -55
  156. package/src/session/session-manager.ts +32 -0
  157. package/src/session/session-storage.ts +68 -8
  158. package/src/session/shake-types.ts +43 -0
  159. package/src/slash-commands/builtin-registry.ts +39 -16
  160. package/src/task/executor.ts +17 -7
  161. package/src/task/index.ts +9 -8
  162. package/src/task/repair-args.ts +117 -0
  163. package/src/tiny/models.ts +2 -2
  164. package/src/tiny/title-client.ts +133 -43
  165. package/src/tiny/title-protocol.ts +10 -5
  166. package/src/tiny/worker.ts +3 -46
  167. package/src/tools/ast-edit.ts +3 -0
  168. package/src/tools/ast-grep.ts +3 -0
  169. package/src/tools/eval.ts +202 -26
  170. package/src/tools/find.ts +20 -6
  171. package/src/tools/gh.ts +1 -0
  172. package/src/tools/grouped-file-output.ts +9 -2
  173. package/src/tools/index.ts +17 -5
  174. package/src/tools/memory-edit.ts +4 -4
  175. package/src/tools/memory-recall.ts +5 -5
  176. package/src/tools/memory-reflect.ts +5 -5
  177. package/src/tools/memory-retain.ts +4 -4
  178. package/src/tools/path-utils.ts +13 -2
  179. package/src/tools/read.ts +1 -0
  180. package/src/tools/render-utils.ts +2 -1
  181. package/src/tools/search.ts +491 -76
  182. package/src/tui/output-block.ts +37 -75
  183. package/src/utils/git.ts +9 -3
  184. package/dist/types/mnemosyne/backend.d.ts +0 -4
  185. package/dist/types/mnemosyne/config.d.ts +0 -29
  186. package/dist/types/mnemosyne/state.d.ts +0 -72
  187. /package/dist/types/{mnemosyne → mnemopi}/index.d.ts +0 -0
  188. /package/src/{mnemosyne → mnemopi}/index.ts +0 -0
@@ -39,11 +39,88 @@ if (!globalThis.__omp_js_prelude_loaded__) {
39
39
  return values.length === 1 ? values[0] : values;
40
40
  };
41
41
 
42
+ const hasOwn = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
43
+
42
44
  const llm = async (prompt, opts = {}) => {
43
45
  const o = toOptions(opts);
44
46
  const res = await globalThis.__omp_call_tool__("__llm__", { prompt, ...o });
45
47
  const text = res && typeof res === "object" ? res.text : res;
46
- return o.schema ? JSON.parse(text) : text;
48
+ return hasOwn(o, "schema") ? JSON.parse(text) : text;
49
+ };
50
+
51
+ const agent = async (prompt, opts = {}) => {
52
+ const o = toOptions(opts);
53
+ const res = await globalThis.__omp_call_tool__("__agent__", { prompt, ...o });
54
+ const text = res && typeof res === "object" ? res.text : res;
55
+ return hasOwn(o, "schema") ? JSON.parse(text) : text;
56
+ };
57
+
58
+ const normalizeConcurrency = value => {
59
+ const number = Number(value ?? 4);
60
+ if (!Number.isFinite(number)) return 4;
61
+ return Math.max(1, Math.min(16, Math.trunc(number)));
62
+ };
63
+
64
+ const __pool = async (items, limit, fn) => {
65
+ const list = Array.from(items ?? []);
66
+ const concurrency = Math.min(normalizeConcurrency(limit), list.length);
67
+ const results = new Array(list.length);
68
+ let next = 0;
69
+ const worker = async () => {
70
+ while (true) {
71
+ const index = next++;
72
+ if (index >= list.length) return;
73
+ results[index] = await fn(list[index], index);
74
+ }
75
+ };
76
+ await Promise.all(Array.from({ length: concurrency }, () => worker()));
77
+ return results;
78
+ };
79
+
80
+ const parallel = async (thunks, opts = {}) =>
81
+ __pool(thunks, toOptions(opts).concurrency, (thunk, index) => {
82
+ if (typeof thunk !== "function") throw new TypeError("parallel() expects an iterable of functions");
83
+ return thunk(index);
84
+ });
85
+
86
+ const pipeline = async (items, ...stagesAndOptions) => {
87
+ let opts = {};
88
+ const last = stagesAndOptions.at(-1);
89
+ if (last && typeof last === "object" && !Array.isArray(last)) {
90
+ opts = last;
91
+ stagesAndOptions = stagesAndOptions.slice(0, -1);
92
+ }
93
+ let current = Array.from(items ?? []);
94
+ for (const stage of stagesAndOptions) {
95
+ if (typeof stage !== "function") throw new TypeError("pipeline() stages must be functions");
96
+ current = await __pool(current, toOptions(opts).concurrency, stage);
97
+ }
98
+ return current;
99
+ };
100
+
101
+ const log = message => globalThis.__omp_emit_status__("log", { message: String(message) });
102
+
103
+ const phase = title => {
104
+ globalThis.__omp_phase__ = String(title);
105
+ globalThis.__omp_emit_status__("phase", { title: String(title) });
106
+ };
107
+
108
+ const __budgetSnap = async () => {
109
+ const r = await globalThis.__omp_call_tool__("__budget__", {});
110
+ return r && typeof r === "object" ? r : {};
111
+ };
112
+
113
+ const budget = {
114
+ total: async () => {
115
+ const s = await __budgetSnap();
116
+ return s.total ?? null;
117
+ },
118
+ spent: async () => Number((await __budgetSnap()).spent ?? 0),
119
+ remaining: async () => {
120
+ const s = await __budgetSnap();
121
+ return s.total == null ? Infinity : Math.max(0, Number(s.total) - Number(s.spent ?? 0));
122
+ },
123
+ hard: async () => Boolean((await __budgetSnap()).hard),
47
124
  };
48
125
 
49
126
  const display = value => {
@@ -70,6 +147,13 @@ if (!globalThis.__omp_js_prelude_loaded__) {
70
147
  globalThis.tool = tool;
71
148
  globalThis.llm = llm;
72
149
  globalThis.output = output;
150
+ globalThis.agent = agent;
151
+ globalThis.parallel = parallel;
152
+ globalThis.pipeline = pipeline;
153
+ globalThis.log = log;
154
+ globalThis.phase = phase;
155
+ globalThis.budget = budget;
156
+ globalThis.__pool = __pool;
73
157
  globalThis.read = read;
74
158
  globalThis.write = write;
75
159
  globalThis.append = append;
@@ -1,6 +1,8 @@
1
1
  import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
2
2
  import type { ToolSession } from "../../tools";
3
3
  import { ToolError } from "../../tools/tool-errors";
4
+ import { EVAL_AGENT_BRIDGE_NAME, runEvalAgent } from "../agent-bridge";
5
+ import { EVAL_BUDGET_BRIDGE_NAME, type EvalBudgetResult, runEvalBudget } from "../budget-bridge";
4
6
  import { EVAL_LLM_BRIDGE_NAME, runEvalLlm } from "../llm-bridge";
5
7
  import type { JsStatusEvent } from "./shared/types";
6
8
 
@@ -14,6 +16,7 @@ interface ToolBridgeOptions {
14
16
 
15
17
  type ToolValue =
16
18
  | string
19
+ | EvalBudgetResult
17
20
  | {
18
21
  text: string;
19
22
  details?: unknown;
@@ -105,6 +108,12 @@ export async function callSessionTool(name: string, args: unknown, options: Tool
105
108
  if (name === EVAL_LLM_BRIDGE_NAME) {
106
109
  return await runEvalLlm(args, options);
107
110
  }
111
+ if (name === EVAL_AGENT_BRIDGE_NAME) {
112
+ return await runEvalAgent(args, options);
113
+ }
114
+ if (name === EVAL_BUDGET_BRIDGE_NAME) {
115
+ return await runEvalBudget(args, options);
116
+ }
108
117
  const tool = getTool(options.session, name);
109
118
  const normalizedArgs = normalizeArgs(args);
110
119
  const toolCallId = `js-${name}-${crypto.randomUUID()}`;
@@ -25,6 +25,12 @@ export interface PythonExecutorOptions {
25
25
  timeoutMs?: number;
26
26
  /** Absolute wall-clock deadline in milliseconds since epoch */
27
27
  deadlineMs?: number;
28
+ /**
29
+ * Inactivity budget (ms). Used only for timeout-annotation text when the
30
+ * caller drives cancellation via an idle-aware `signal` instead of a
31
+ * wall-clock `deadlineMs`/`timeoutMs`. Does not arm a timer.
32
+ */
33
+ idleTimeoutMs?: number;
28
34
  /** Callback for streaming output chunks (already sanitized) */
29
35
  onChunk?: (chunk: string) => Promise<void> | void;
30
36
  /** AbortSignal for cancellation */
@@ -57,6 +63,13 @@ export interface PythonExecutorOptions {
57
63
  toolSession?: ToolSession;
58
64
  /** Callback for status events emitted by tool bridge invocations. */
59
65
  emitStatus?: (event: JsStatusEvent) => void;
66
+ /**
67
+ * Live status events streamed as they are emitted (both host-side bridge
68
+ * helpers like `agent()` and kernel-side `display`/`log`/`phase`). Mirrors
69
+ * what lands in `displayOutputs` so callers can render progress before the
70
+ * cell finishes.
71
+ */
72
+ onStatus?: (event: JsStatusEvent) => void;
60
73
  /** @internal Bridge session id, set by `executePython` before delegating. */
61
74
  bridgeSessionId?: string;
62
75
  /** @internal Bridge endpoint info, set by `executePython` before delegating. */
@@ -218,18 +231,20 @@ async function waitForPromiseWithCancellation<T>(
218
231
  // Result formatting
219
232
  // ---------------------------------------------------------------------------
220
233
 
221
- function formatTimeoutAnnotation(timeoutMs?: number): string | undefined {
234
+ function formatTimeoutAnnotation(timeoutMs?: number, idle = false): string | undefined {
235
+ const suffix = idle ? " of inactivity" : "";
222
236
  if (timeoutMs === undefined) return "Command timed out";
223
237
  const secs = Math.max(1, Math.round(timeoutMs / 1000));
224
- return `Command timed out after ${secs} seconds`;
238
+ return `Command timed out after ${secs} seconds${suffix}`;
225
239
  }
226
240
 
227
- function formatKernelTimeoutAnnotation(timeoutMs: number | undefined, kernelKilled: boolean): string {
241
+ function formatKernelTimeoutAnnotation(timeoutMs: number | undefined, kernelKilled: boolean, idle = false): string {
228
242
  const secs = timeoutMs === undefined ? undefined : Math.max(1, Math.round(timeoutMs / 1000));
243
+ const suffix = idle ? " of inactivity" : "";
229
244
  if (kernelKilled) {
230
- return "eval cell timed out and the kernel was unresponsive to interrupt; the kernel has been killed and will be recreated on the next call.";
245
+ return `eval cell timed out${suffix} and the kernel was unresponsive to interrupt; the kernel has been killed and will be recreated on the next call.`;
231
246
  }
232
- const duration = secs === undefined ? "the configured timeout" : `${secs}s`;
247
+ const duration = secs === undefined ? "the configured timeout" : `${secs}s${suffix}`;
233
248
  return `eval cell timed out after ${duration}; kernel interrupted but remains running. Reset the kernel via { reset: true } if state appears corrupted.`;
234
249
  }
235
250
 
@@ -473,12 +488,18 @@ async function executeWithKernel(
473
488
  const displayOutputs: KernelDisplayOutput[] = [];
474
489
  const deadlineMs = getExecutionDeadlineMs(options);
475
490
  let executionTimeoutMs: number | undefined;
476
-
477
- const emitStatus =
478
- options?.emitStatus ??
479
- ((event: JsStatusEvent) => {
480
- displayOutputs.push({ type: "status", event });
481
- });
491
+ // Idle mode: the caller (eval tool) drives cancellation via an idle-aware
492
+ // signal and passes no wall-clock deadline, so annotate timeouts with the
493
+ // configured inactivity budget rather than a remaining-deadline figure.
494
+ const idleMode = deadlineMs === undefined && options?.idleTimeoutMs !== undefined;
495
+
496
+ // Collect every display output and, for status events, stream them live so
497
+ // long-running bridge helpers (e.g. `agent()`) surface progress mid-cell.
498
+ const collectDisplay = (output: KernelDisplayOutput) => {
499
+ displayOutputs.push(output);
500
+ if (output.type === "status") options?.onStatus?.(output.event);
501
+ };
502
+ const emitStatus = options?.emitStatus ?? ((event: JsStatusEvent) => collectDisplay({ type: "status", event }));
482
503
  const runId = `py-${crypto.randomUUID()}`;
483
504
  const unregisterBridge =
484
505
  options?.toolSession && options?.bridgeSessionId
@@ -498,12 +519,16 @@ async function executeWithKernel(
498
519
  signal: options?.signal,
499
520
  timeoutMs: executionTimeoutMs,
500
521
  onChunk: text => sink.push(text),
501
- onDisplay: output => void displayOutputs.push(output),
522
+ onDisplay: output => collectDisplay(output),
502
523
  });
503
524
 
504
525
  if (result.cancelled) {
505
526
  const annotation = result.timedOut
506
- ? formatKernelTimeoutAnnotation(executionTimeoutMs, result.kernelKilled ?? false)
527
+ ? formatKernelTimeoutAnnotation(
528
+ executionTimeoutMs ?? options?.idleTimeoutMs,
529
+ result.kernelKilled ?? false,
530
+ idleMode,
531
+ )
507
532
  : undefined;
508
533
  return {
509
534
  exitCode: undefined,
@@ -540,7 +565,9 @@ async function executeWithKernel(
540
565
  cancelled: true,
541
566
  displayOutputs,
542
567
  stdinRequested: false,
543
- ...(await sink.dump(timedOut ? formatTimeoutAnnotation(executionTimeoutMs) : undefined)),
568
+ ...(await sink.dump(
569
+ timedOut ? formatTimeoutAnnotation(executionTimeoutMs ?? options?.idleTimeoutMs, idleMode) : undefined,
570
+ )),
544
571
  };
545
572
  }
546
573
  const error = err instanceof Error ? err : new Error(String(err));
@@ -28,7 +28,7 @@ export default {
28
28
  const kernelMode = readSetting<PythonExecutorOptions["kernelMode"]>(opts.session, "python.kernelMode");
29
29
  const executorOptions: PythonExecutorOptions = {
30
30
  cwd: opts.cwd,
31
- deadlineMs: opts.deadlineMs,
31
+ idleTimeoutMs: opts.idleTimeoutMs,
32
32
  signal: opts.signal,
33
33
  sessionId: namespaceSessionId(opts.sessionId),
34
34
  kernelMode,
@@ -39,6 +39,7 @@ export default {
39
39
  artifactPath: opts.artifactPath,
40
40
  artifactId: opts.artifactId,
41
41
  onChunk: opts.onChunk,
42
+ onStatus: opts.onStatus,
42
43
  toolSession: opts.session,
43
44
  };
44
45
  const result = await executePython(code, executorOptions);
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  if "__omp_prelude_loaded__" not in globals():
4
4
  __omp_prelude_loaded__ = True
5
5
  from pathlib import Path
6
- import os, json
6
+ import os, json, math
7
7
 
8
8
  # __omp_display is injected by runner.py before the prelude executes; it
9
9
  # mirrors IPython's display() semantics with the same MIME bundle output.
@@ -479,3 +479,134 @@ if "__omp_prelude_loaded__" not in globals():
479
479
  res = _bridge_call("__llm__", args)
480
480
  text = res.get("text") if isinstance(res, dict) else res
481
481
  return json.loads(text) if schema is not None else text
482
+
483
+ def agent(prompt, *, agent_type="task", model=None, context=None, label=None, schema=None):
484
+ """Run a subagent and return its final output.
485
+
486
+ `agent_type` selects the subagent definition (default "task"). Pass
487
+ `model` to override that agent's model, `context` for shared background,
488
+ `label` for the output artifact id, and `schema` to request structured
489
+ JSON output; when `schema` is supplied the parsed object is returned.
490
+ """
491
+ args = {"prompt": prompt}
492
+ if agent_type is not None:
493
+ args["agentType"] = agent_type
494
+ if model is not None:
495
+ args["model"] = model
496
+ if context is not None:
497
+ args["context"] = context
498
+ if label is not None:
499
+ args["label"] = label
500
+ if schema is not None:
501
+ args["schema"] = schema
502
+ res = _bridge_call("__agent__", args)
503
+ text = res.get("text") if isinstance(res, dict) else res
504
+ return json.loads(text) if schema is not None else text
505
+
506
+ def _normalize_concurrency(value):
507
+ """Clamp a concurrency hint to [1, 16], defaulting to 4 on bad input."""
508
+ try:
509
+ n = int(value)
510
+ except (TypeError, ValueError):
511
+ n = 4
512
+ return max(1, min(16, n))
513
+
514
+ def _pool_map(items, fn, concurrency):
515
+ """Run ``fn`` over ``items`` through a bounded thread pool.
516
+
517
+ Preserves input order, barriers until every task settles, and raises the
518
+ lowest-index exception if any task failed. Each task runs inside a copy
519
+ of the submitting thread's context so the ``_CURRENT_RID`` ContextVar
520
+ propagates and bridge calls (agent(), tool.*, etc.) keep working.
521
+ """
522
+ import concurrent.futures, contextvars
523
+ items = list(items)
524
+ if not items:
525
+ return []
526
+ workers = min(_normalize_concurrency(concurrency), len(items))
527
+ results = [None] * len(items)
528
+ errors = {}
529
+ with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool:
530
+ futures = {}
531
+ for i, item in enumerate(items):
532
+ ctx = contextvars.copy_context()
533
+ futures[pool.submit(ctx.run, fn, item)] = i
534
+ for fut in concurrent.futures.as_completed(futures):
535
+ i = futures[fut]
536
+ try:
537
+ results[i] = fut.result()
538
+ except BaseException as exc: # noqa: BLE001 - propagate to caller
539
+ errors[i] = exc
540
+ if errors:
541
+ raise errors[min(errors)]
542
+ return results
543
+
544
+ def parallel(thunks, *, concurrency=4):
545
+ """Run zero-arg callables through a bounded pool, preserving input order.
546
+
547
+ Barriers until all finish; re-raises the lowest-index exception if any
548
+ thunk raised.
549
+ """
550
+ thunks = list(thunks)
551
+ for t in thunks:
552
+ if not callable(t):
553
+ raise TypeError("parallel() expects an iterable of zero-arg callables")
554
+ return _pool_map(thunks, lambda t: t(), concurrency)
555
+
556
+ def pipeline(items, *stages, concurrency=4):
557
+ """Map items left-to-right through one-arg stage callables.
558
+
559
+ Every item clears stage N before any item enters stage N+1 (barrier per
560
+ stage). Stage 1 receives the original item; later stages receive the
561
+ previous stage's result.
562
+ """
563
+ current = list(items)
564
+ for stage in stages:
565
+ if not callable(stage):
566
+ raise TypeError("pipeline() stages must be callables")
567
+ current = _pool_map(current, stage, concurrency)
568
+ return current
569
+
570
+ def log(message):
571
+ """Emit a status ``log`` event for TUI rendering."""
572
+ _emit_status("log", message=str(message))
573
+ return None
574
+
575
+ def phase(title):
576
+ """Record the current readable phase and emit a status ``phase`` event."""
577
+ globals()["__omp_current_phase__"] = str(title)
578
+ _emit_status("phase", title=str(title))
579
+ return None
580
+
581
+ class _Budget:
582
+ """Live view of the host Goal Mode token budget via the host bridge."""
583
+
584
+ @property
585
+ def total(self):
586
+ snap = _bridge_call("__budget__", {})
587
+ return (snap or {}).get("total")
588
+
589
+ @property
590
+ def hard(self):
591
+ snap = _bridge_call("__budget__", {})
592
+ return bool((snap or {}).get("hard"))
593
+
594
+ def spent(self):
595
+ snap = _bridge_call("__budget__", {})
596
+ return int((snap or {}).get("spent") or 0)
597
+
598
+ def remaining(self):
599
+ snap = _bridge_call("__budget__", {}) or {}
600
+ total = snap.get("total")
601
+ if total is None:
602
+ return math.inf
603
+ return max(0, total - int(snap.get("spent") or 0))
604
+
605
+ def __repr__(self):
606
+ try:
607
+ snap = _bridge_call("__budget__", {}) or {}
608
+ return f"<budget total={snap.get('total')} spent={snap.get('spent')}>"
609
+ except Exception:
610
+ return "<budget unavailable>"
611
+
612
+ budget = _Budget()
@@ -16,6 +16,7 @@ export interface BashExecutorOptions {
16
16
  cwd?: string;
17
17
  timeout?: number;
18
18
  onChunk?: (chunk: string) => void;
19
+ chunkThrottleMs?: number;
19
20
  signal?: AbortSignal;
20
21
  /** Session key suffix to isolate shell sessions per agent */
21
22
  sessionKey?: string;
@@ -97,9 +98,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
97
98
  artifactId: options?.artifactId,
98
99
  headBytes: resolveOutputSinkHeadBytes(settings),
99
100
  maxColumns: resolveOutputMaxColumns(settings),
100
- // Throttle the streaming preview callback to avoid saturating the
101
- // event loop when commands produce massive output (e.g. seq 1 50M).
102
- chunkThrottleMs: options?.onChunk ? 50 : 0,
101
+ chunkThrottleMs: options?.onChunk ? (options.chunkThrottleMs ?? 50) : 0,
103
102
  });
104
103
 
105
104
  // sink.push() is synchronous — buffer management, counters, and onChunk
@@ -101,11 +101,11 @@ export type CustomToolSessionEvent =
101
101
  | {
102
102
  reason: "auto_compaction_start";
103
103
  trigger: "threshold" | "overflow" | "idle" | "incomplete";
104
- action: "context-full" | "handoff";
104
+ action: "context-full" | "handoff" | "shake";
105
105
  }
106
106
  | {
107
107
  reason: "auto_compaction_end";
108
- action: "context-full" | "handoff";
108
+ action: "context-full" | "handoff" | "shake";
109
109
  result: CompactionResult | undefined;
110
110
  aborted: boolean;
111
111
  willRetry: boolean;
@@ -315,9 +315,15 @@ export class ExtensionRunner {
315
315
  return tools;
316
316
  }
317
317
 
318
- getFlags(): Map<string, ExtensionFlag> {
318
+ /**
319
+ * Aggregate the registered CLI flags across a set of extensions (last write
320
+ * wins on name collision). Static so callers that need the flag set before a
321
+ * runner exists — e.g. the CLI resolving `@file`/flag args before session
322
+ * creation — share this exact logic instead of duplicating it.
323
+ */
324
+ static aggregateFlags(extensions: readonly Extension[]): Map<string, ExtensionFlag> {
319
325
  const allFlags = new Map<string, ExtensionFlag>();
320
- for (const ext of this.extensions) {
326
+ for (const ext of extensions) {
321
327
  for (const [name, flag] of ext.flags) {
322
328
  allFlags.set(name, flag);
323
329
  }
@@ -325,6 +331,10 @@ export class ExtensionRunner {
325
331
  return allFlags;
326
332
  }
327
333
 
334
+ getFlags(): Map<string, ExtensionFlag> {
335
+ return ExtensionRunner.aggregateFlags(this.extensions);
336
+ }
337
+
328
338
  getFlagValues(): Map<string, boolean | string> {
329
339
  return new Map(this.runtime.flagValues);
330
340
  }
@@ -25,6 +25,29 @@ const KNOWN_HOSTS: Record<string, (pathname: string, hash: string) => { user: st
25
25
  "codeberg.org": extractStandard,
26
26
  };
27
27
 
28
+ /**
29
+ * Namespaced shorthand prefixes accepted by `omp plugin install`, mapped to
30
+ * their canonical host. `PluginManager.install` normalizes non-GitHub prefixes
31
+ * before invoking bun because bun only treats `github:` as a hosted shorthand.
32
+ */
33
+ const SHORTHAND_PREFIXES: Record<string, string> = {
34
+ github: "github.com",
35
+ gitlab: "gitlab.com",
36
+ bitbucket: "bitbucket.org",
37
+ codeberg: "codeberg.org",
38
+ sourcehut: "git.sr.ht",
39
+ srht: "git.sr.ht",
40
+ };
41
+
42
+ /**
43
+ * `<prefix>:<user>/<repo>[.git][#<ref>]` shape. `<repo>` is non-greedy so the
44
+ * optional `.git` suffix and `#ref` tail bind tightly; `<repo>` may itself
45
+ * contain `/` to support nested GitLab groups (`gitlab:group/sub/project`).
46
+ * `<user>` rejects `/`, `:`, `#` to keep protocol URLs (`https://…`) and
47
+ * scp-like SSH (`git@github.com:user/repo`) out of this path.
48
+ */
49
+ const SHORTHAND_RE = /^([a-z]+):([^/:#]+)\/([^#]+?)(?:\.git)?(?:#(.+))?$/i;
50
+
28
51
  function stripUrlCredentials(url: string): string {
29
52
  if (!url.includes("://")) return url;
30
53
  try {
@@ -209,14 +232,55 @@ function parseGenericGitUrl(url: string): GitSource | null {
209
232
  return { type: "git", repo, host, path: normalizedPath, ref, pinned: Boolean(ref) };
210
233
  }
211
234
 
235
+ /**
236
+ * Match an npm/bun-style namespaced shorthand (`github:user/repo`, optionally
237
+ * `…#ref` or `….git`). Returns null for protocol URLs and any prefix not in
238
+ * `SHORTHAND_PREFIXES` so the caller can fall through to the generic paths.
239
+ */
240
+ function tryNamespacedShorthand(trimmed: string): GitSource | null {
241
+ // Cheap gate: bail out before touching protocol URLs (`https://`, `ssh://`,
242
+ // `git://`) where the char after the colon is always `/`. The shorthand we
243
+ // care about never starts with `<scheme>://`.
244
+ if (!/^[a-z]+:[^/]/i.test(trimmed)) return null;
245
+ const match = trimmed.match(SHORTHAND_RE);
246
+ if (!match) return null;
247
+ const prefix = (match[1] ?? "").toLowerCase();
248
+ const host = SHORTHAND_PREFIXES[prefix];
249
+ if (!host) return null;
250
+ const user = match[2] ?? "";
251
+ const repoPath = match[3] ?? "";
252
+ if (!user || !repoPath) return null;
253
+ const ref = match[4];
254
+ if (ref) {
255
+ try {
256
+ decodeURIComponent(ref);
257
+ } catch {
258
+ return null;
259
+ }
260
+ }
261
+ const fullPath = `${user}/${repoPath}`;
262
+ return {
263
+ type: "git",
264
+ repo: `https://${host}/${fullPath}`,
265
+ host,
266
+ path: fullPath,
267
+ ref: ref || undefined,
268
+ pinned: Boolean(ref),
269
+ };
270
+ }
271
+
212
272
  /**
213
273
  * Parse git source into a GitSource.
214
274
  *
215
275
  * Rules:
216
- * - With `git:` prefix, accept shorthand forms.
276
+ * - Namespaced shorthand (`github:user/repo`, `gitlab:`, `bitbucket:`,
277
+ * `codeberg:`, `sourcehut:`/`srht:`) is accepted directly; installers should
278
+ * normalize entries that bun does not understand natively.
279
+ * - With `git:` prefix, accept generic shorthand forms.
217
280
  * - Without `git:` prefix, only accept explicit protocol URLs.
218
281
  *
219
282
  * Handles:
283
+ * - `github:user/repo[#ref]`-style namespaced shorthand
220
284
  * - `git:` prefixed URLs (`git:github.com/user/repo`)
221
285
  * - SSH SCP-like URLs (`git:git@github.com:user/repo`)
222
286
  * - HTTPS/HTTP/SSH/git protocol URLs
@@ -227,10 +291,23 @@ function parseGenericGitUrl(url: string): GitSource | null {
227
291
  */
228
292
  export function parseGitUrl(source: string): GitSource | null {
229
293
  const trimmed = source.trim();
230
- const hasGitPrefix = /^git:(?!\/\/)/i.test(trimmed);
231
- const url = hasGitPrefix ? trimmed.slice(4).trim() : trimmed;
232
294
 
233
- if (!hasGitPrefix && !/^(https?|ssh|git):\/\//i.test(url)) {
295
+ const shorthand = tryNamespacedShorthand(trimmed);
296
+ if (shorthand) return shorthand;
297
+
298
+ // Strip the `git+` URL prefix that npm/bun accept (`git+https://…`,
299
+ // `git+ssh://…`, `git+git://…`). The rest of the pipeline only deals with
300
+ // bare schemes.
301
+ const stripped = /^git\+/i.test(trimmed) ? trimmed.slice(4) : trimmed;
302
+
303
+ const hasGitPrefix = /^git:(?!\/\/)/i.test(stripped);
304
+ const url = hasGitPrefix ? stripped.slice(4).trim() : stripped;
305
+
306
+ // Accept: explicit protocol URL, `git:` shorthand, or scp-like SSH
307
+ // (`git@host:user/repo`). The scp form is unambiguous — no local path
308
+ // starts with `git@` — and matches the syntax that `git clone` itself
309
+ // accepts, which `bun install` forwards through.
310
+ if (!hasGitPrefix && !/^(https?|ssh|git):\/\//i.test(url) && !/^git@[^:]+:.+\/.+/i.test(url)) {
234
311
  return null;
235
312
  }
236
313
 
@@ -279,3 +356,12 @@ export function parseGitUrl(source: string): GitSource | null {
279
356
 
280
357
  return parseGenericGitUrl(url);
281
358
  }
359
+
360
+ /**
361
+ * Returns true if the spec is parseable as a git source (protocol URL,
362
+ * scp-like SSH wrapped in `git:`, plain `git:` shorthand, or namespaced
363
+ * shorthand like `github:user/repo`). The inverse of "this is an npm spec".
364
+ */
365
+ export function isGitSpec(spec: string): boolean {
366
+ return parseGitUrl(spec) !== null;
367
+ }