@oh-my-pi/pi-coding-agent 17.0.1 → 17.0.3
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 +162 -40
- package/dist/cli.js +4559 -4543
- package/dist/types/advisor/config.d.ts +14 -6
- package/dist/types/advisor/runtime.d.ts +20 -11
- package/dist/types/autolearn/controller.d.ts +1 -0
- package/dist/types/config/model-discovery.d.ts +17 -0
- package/dist/types/config/model-resolver.d.ts +6 -2
- package/dist/types/config/settings-schema.d.ts +39 -10
- package/dist/types/config/settings.d.ts +50 -0
- package/dist/types/cursor.d.ts +11 -0
- package/dist/types/discovery/helpers.d.ts +5 -2
- package/dist/types/eval/agent-bridge.d.ts +7 -19
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
- package/dist/types/extensibility/extensions/runner.d.ts +7 -0
- package/dist/types/extensibility/extensions/types.d.ts +18 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +55 -1
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/extensibility/utils.d.ts +2 -2
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/lsp/types.d.ts +3 -0
- package/dist/types/mcp/transports/stdio.d.ts +42 -1
- package/dist/types/modes/components/advisor-config.d.ts +8 -1
- package/dist/types/modes/components/agent-hub.d.ts +15 -0
- package/dist/types/modes/components/hook-editor.d.ts +7 -0
- package/dist/types/modes/components/model-hub.d.ts +5 -2
- package/dist/types/modes/components/session-selector.d.ts +2 -0
- package/dist/types/modes/controllers/command-controller.d.ts +8 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
- package/dist/types/modes/print-mode.d.ts +1 -1
- package/dist/types/modes/warp-events.d.ts +24 -0
- package/dist/types/modes/warp-events.test.d.ts +1 -0
- package/dist/types/plan-mode/model-transition.d.ts +47 -0
- package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
- package/dist/types/registry/agent-lifecycle.d.ts +26 -1
- package/dist/types/registry/persisted-agents.d.ts +3 -0
- package/dist/types/sdk.d.ts +27 -5
- package/dist/types/session/agent-session.d.ts +34 -10
- package/dist/types/session/session-entries.d.ts +6 -1
- package/dist/types/session/session-history-format.d.ts +10 -0
- package/dist/types/session/session-manager.d.ts +19 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
- package/dist/types/task/executor.d.ts +26 -1
- package/dist/types/task/index.d.ts +1 -1
- package/dist/types/task/label.d.ts +1 -1
- package/dist/types/task/parallel.d.ts +14 -0
- package/dist/types/task/structured-subagent.d.ts +111 -0
- package/dist/types/task/types.d.ts +51 -0
- package/dist/types/telemetry-export.d.ts +34 -9
- package/dist/types/tools/approval.d.ts +8 -0
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/essential-tools.d.ts +29 -0
- package/dist/types/tools/fetch.d.ts +4 -5
- package/dist/types/tools/hub/messaging.d.ts +5 -1
- package/dist/types/tools/image-gen.d.ts +2 -1
- package/dist/types/tools/index.d.ts +17 -1
- package/dist/types/tools/todo.d.ts +31 -0
- package/dist/types/tools/xdev.d.ts +11 -2
- package/dist/types/tui/tree-list.d.ts +7 -0
- package/dist/types/utils/markit.d.ts +11 -0
- package/dist/types/utils/title-generator.d.ts +2 -1
- package/dist/types/web/search/providers/kimi.d.ts +4 -1
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +21 -16
- package/src/advisor/__tests__/advisor.test.ts +1304 -42
- package/src/advisor/__tests__/config.test.ts +58 -2
- package/src/advisor/config.ts +76 -24
- package/src/advisor/runtime.ts +445 -92
- package/src/autolearn/controller.ts +23 -28
- package/src/cli/file-processor.ts +1 -2
- package/src/cli.ts +5 -1
- package/src/config/model-discovery.ts +81 -21
- package/src/config/model-registry.ts +25 -6
- package/src/config/model-resolver.ts +14 -7
- package/src/config/settings-schema.ts +46 -9
- package/src/config/settings.ts +405 -25
- package/src/cursor.ts +20 -3
- package/src/debug/report-bundle.ts +40 -4
- package/src/discovery/helpers.ts +28 -5
- package/src/eval/__tests__/agent-bridge.test.ts +133 -47
- package/src/eval/__tests__/prelude-agent.test.ts +29 -0
- package/src/eval/agent-bridge.ts +104 -477
- package/src/eval/jl/prelude.jl +7 -6
- package/src/eval/js/shared/prelude.txt +5 -4
- package/src/eval/py/prelude.py +11 -39
- package/src/eval/rb/prelude.rb +5 -6
- package/src/exec/bash-executor.ts +14 -5
- package/src/extensibility/custom-tools/loader.ts +3 -3
- package/src/extensibility/custom-tools/wrapper.ts +2 -1
- package/src/extensibility/extensions/loader.ts +3 -3
- package/src/extensibility/extensions/managed-timers.ts +83 -0
- package/src/extensibility/extensions/runner.ts +26 -0
- package/src/extensibility/extensions/types.ts +18 -0
- package/src/extensibility/extensions/wrapper.ts +2 -1
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +149 -8
- package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
- package/src/extensibility/plugins/manager.ts +2 -2
- package/src/extensibility/shared-events.ts +6 -0
- package/src/extensibility/utils.ts +91 -25
- package/src/irc/bus.ts +22 -3
- package/src/launch/broker.ts +3 -2
- package/src/launch/client.ts +2 -2
- package/src/launch/presence.ts +2 -2
- package/src/lsp/client.ts +58 -1
- package/src/lsp/index.ts +62 -6
- package/src/lsp/types.ts +3 -0
- package/src/main.ts +11 -8
- package/src/mcp/manager.ts +9 -3
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/mcp/transports/stdio.test.ts +269 -1
- package/src/mcp/transports/stdio.ts +255 -24
- package/src/modes/components/advisor-config.ts +65 -3
- package/src/modes/components/agent-hub.ts +1 -72
- package/src/modes/components/ask-dialog.ts +1 -1
- package/src/modes/components/bash-execution.ts +7 -3
- package/src/modes/components/eval-execution.ts +3 -1
- package/src/modes/components/hook-editor.ts +18 -3
- package/src/modes/components/model-hub.ts +138 -42
- package/src/modes/components/session-selector.ts +4 -0
- package/src/modes/components/status-line/component.test.ts +1 -0
- package/src/modes/components/status-line/segments.ts +21 -6
- package/src/modes/controllers/command-controller.ts +167 -47
- package/src/modes/controllers/event-controller.ts +5 -0
- package/src/modes/controllers/extension-ui-controller.ts +4 -22
- package/src/modes/controllers/input-controller.ts +12 -12
- package/src/modes/controllers/selector-controller.ts +191 -31
- package/src/modes/interactive-mode.ts +169 -62
- package/src/modes/print-mode.ts +3 -3
- package/src/modes/rpc/host-tools.ts +2 -1
- package/src/modes/rpc/rpc-mode.ts +19 -4
- package/src/modes/warp-events.test.ts +794 -0
- package/src/modes/warp-events.ts +232 -0
- package/src/plan-mode/model-transition.test.ts +60 -0
- package/src/plan-mode/model-transition.ts +51 -0
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/tools/eval.md +5 -2
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/task.md +12 -8
- package/src/prompts/tools/write.md +1 -1
- package/src/registry/agent-lifecycle.ts +133 -18
- package/src/registry/persisted-agents.ts +74 -0
- package/src/sdk.ts +343 -122
- package/src/session/agent-session.ts +1359 -368
- package/src/session/session-entries.ts +6 -1
- package/src/session/session-history-format.ts +20 -5
- package/src/session/session-manager.ts +57 -0
- package/src/session/streaming-output.ts +41 -1
- package/src/slash-commands/builtin-registry.ts +7 -0
- package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
- package/src/system-prompt.ts +7 -2
- package/src/task/executor.ts +100 -22
- package/src/task/index.ts +258 -429
- package/src/task/label.ts +2 -0
- package/src/task/parallel.ts +43 -0
- package/src/task/persisted-revive.ts +19 -7
- package/src/task/structured-subagent.ts +642 -0
- package/src/task/types.ts +58 -0
- package/src/telemetry-export.ts +453 -97
- package/src/tools/__tests__/eval-description.test.ts +1 -1
- package/src/tools/approval.ts +11 -0
- package/src/tools/bash.ts +71 -38
- package/src/tools/essential-tools.ts +45 -0
- package/src/tools/fetch.ts +28 -105
- package/src/tools/gh.ts +169 -2
- package/src/tools/hub/messaging.ts +16 -3
- package/src/tools/image-gen.ts +69 -7
- package/src/tools/index.ts +50 -15
- package/src/tools/path-utils.ts +1 -0
- package/src/tools/read.ts +62 -29
- package/src/tools/todo.ts +126 -13
- package/src/tools/write.ts +22 -4
- package/src/tools/xdev.ts +14 -3
- package/src/tui/tree-list.ts +39 -0
- package/src/utils/markit.ts +12 -0
- package/src/utils/title-generator.ts +15 -4
- package/src/utils/zip.ts +16 -1
- package/src/web/search/providers/kimi.ts +18 -12
- package/src/web/search/types.ts +6 -1
package/src/eval/jl/prelude.jl
CHANGED
|
@@ -519,7 +519,7 @@ function completion(prompt::String; model="default", system=nothing, schema=noth
|
|
|
519
519
|
return schema === nothing ? text : Main.json_parse(string(text))
|
|
520
520
|
end
|
|
521
521
|
|
|
522
|
-
function agent(prompt::String; agent="task", model=nothing, label=nothing, schema=nothing, isolated=nothing, apply=nothing, merge=nothing, handle=false, kwargs...)
|
|
522
|
+
function agent(prompt::String; agent="task", model=nothing, label=nothing, schema=nothing, schema_mode=nothing, isolated=nothing, apply=nothing, merge=nothing, handle=false, kwargs...)
|
|
523
523
|
args_dict = Dict{String, Any}("prompt" => prompt)
|
|
524
524
|
if agent !== nothing
|
|
525
525
|
args_dict["agent"] = agent
|
|
@@ -533,8 +533,9 @@ function agent(prompt::String; agent="task", model=nothing, label=nothing, schem
|
|
|
533
533
|
if schema !== nothing
|
|
534
534
|
args_dict["schema"] = schema
|
|
535
535
|
end
|
|
536
|
-
|
|
537
|
-
|
|
536
|
+
if schema_mode !== nothing
|
|
537
|
+
args_dict["schemaMode"] = schema_mode
|
|
538
|
+
end
|
|
538
539
|
if isolated !== nothing
|
|
539
540
|
args_dict["isolated"] = Bool(isolated)
|
|
540
541
|
end
|
|
@@ -548,13 +549,13 @@ function agent(prompt::String; agent="task", model=nothing, label=nothing, schem
|
|
|
548
549
|
for (k, v) in kwargs
|
|
549
550
|
args_dict[string(k)] = v
|
|
550
551
|
end
|
|
551
|
-
# Tell the bridge a handle is wanted so it preserves the backing artifacts.
|
|
552
552
|
if handle_result
|
|
553
553
|
args_dict["handle"] = true
|
|
554
554
|
end
|
|
555
555
|
res = __omp_call_bridge("__agent__", args_dict)
|
|
556
556
|
text = res isa AbstractDict ? get(res, "text", res) : res
|
|
557
|
-
|
|
557
|
+
has_data = res isa AbstractDict && haskey(res, "data")
|
|
558
|
+
parsed = has_data ? res["data"] : (schema === nothing ? text : Main.json_parse(string(text)))
|
|
558
559
|
if !handle_result
|
|
559
560
|
return parsed
|
|
560
561
|
end
|
|
@@ -569,7 +570,7 @@ function agent(prompt::String; agent="task", model=nothing, label=nothing, schem
|
|
|
569
570
|
"id" => get(details, "id", nothing),
|
|
570
571
|
"agent" => get(details, "agent", nothing)
|
|
571
572
|
)
|
|
572
|
-
if schema !== nothing
|
|
573
|
+
if has_data || schema !== nothing
|
|
573
574
|
node["data"] = parsed
|
|
574
575
|
end
|
|
575
576
|
for (src_key, dst_key) in (
|
|
@@ -104,20 +104,21 @@ if (!globalThis.__omp_js_prelude_loaded__) {
|
|
|
104
104
|
"agent",
|
|
105
105
|
opts,
|
|
106
106
|
rest,
|
|
107
|
-
["agent", "model", "label", "schema", "isolated", "apply", "merge"],
|
|
108
|
-
"{ agent, model, label, schema, isolated, apply, merge, handle }",
|
|
107
|
+
["agent", "model", "label", "schema", "isolated", "apply", "merge", "schemaMode"],
|
|
108
|
+
"{ agent, model, label, schema, isolated, apply, merge, schemaMode, handle }",
|
|
109
109
|
);
|
|
110
110
|
const { handle, ...callArgs } = o;
|
|
111
111
|
const res = await globalThis.__omp_call_tool__("__agent__", { prompt, ...callArgs, handle: Boolean(handle) });
|
|
112
112
|
const text = res && typeof res === "object" ? res.text : res;
|
|
113
|
-
const
|
|
113
|
+
const hasData = res && typeof res === "object" && hasOwn(res, "data");
|
|
114
|
+
const parsed = hasData ? res.data : hasOwn(callArgs, "schema") ? JSON.parse(text) : text;
|
|
114
115
|
if (!handle) return parsed;
|
|
115
116
|
const details = res && typeof res === "object" ? res.details : undefined;
|
|
116
117
|
if (!details || typeof details !== "object" || details.id == null) {
|
|
117
118
|
return { text, output: text, handle: null, id: null, agent: null };
|
|
118
119
|
}
|
|
119
120
|
const node = { text, output: text, handle: `agent://${details.id}`, id: details.id, agent: details.agent ?? null };
|
|
120
|
-
if (hasOwn(callArgs, "schema")) node.data = parsed;
|
|
121
|
+
if (hasData || hasOwn(callArgs, "schema")) node.data = parsed;
|
|
121
122
|
for (const key of ["isolated", "patchPath", "branchName", "nestedPatches", "changesApplied", "isolationSummary"]) {
|
|
122
123
|
if (details[key] !== undefined) node[key] = details[key];
|
|
123
124
|
}
|
package/src/eval/py/prelude.py
CHANGED
|
@@ -487,48 +487,17 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
487
487
|
model=None,
|
|
488
488
|
label=None,
|
|
489
489
|
schema=None,
|
|
490
|
+
schema_mode=None,
|
|
490
491
|
isolated=None,
|
|
491
492
|
apply=None,
|
|
492
493
|
merge=None,
|
|
493
494
|
handle=False,
|
|
494
495
|
):
|
|
495
|
-
"""Run a subagent and return its final output.
|
|
496
|
-
|
|
497
|
-
`
|
|
498
|
-
`
|
|
499
|
-
|
|
500
|
-
supplied the parsed object is returned. Share background by writing a
|
|
501
|
-
local:// file and referencing it in the prompt.
|
|
502
|
-
|
|
503
|
-
Pass `isolated=True` to run the subagent inside an isolation worktree
|
|
504
|
-
(copy-on-write of the parent repo) so parallel `agent()` spawns can
|
|
505
|
-
edit overlapping files safely. Strict opt-in, mirroring the `task`
|
|
506
|
-
tool: the default is non-isolated regardless of `task.isolation.mode`.
|
|
507
|
-
`isolated=True` while the setting is `"none"` errors out instead of
|
|
508
|
-
silently downgrading.
|
|
509
|
-
|
|
510
|
-
When isolated, `apply=False` keeps captured changes inside the
|
|
511
|
-
worktree and surfaces the root patch path, branch name, and nested
|
|
512
|
-
repository patches through the DAG node dict (combine with
|
|
513
|
-
`handle=True` to receive them — see below; the bare return type
|
|
514
|
-
stays bytes/string/parsed object and has nowhere to expose artifacts).
|
|
515
|
-
`merge=False` forces patch mode even when `task.isolation.merge` is
|
|
516
|
-
`"branch"`, avoiding the per-call git lock + repo mutation that branch
|
|
517
|
-
mode performs.
|
|
518
|
-
|
|
519
|
-
Set `handle=True` to receive a DAG node dict instead of bare
|
|
520
|
-
text: ``{"text", "output", "handle", "id", "agent"}`` where ``handle``
|
|
521
|
-
is the spawned agent's recoverable ``agent://<id>`` URI. A downstream
|
|
522
|
-
``pipeline``/``parallel`` stage embeds that ``handle`` (or ``output``)
|
|
523
|
-
in its prompt so a large transcript flows through the graph by
|
|
524
|
-
reference, never re-inlined. When ``schema`` is also set the parsed
|
|
525
|
-
object lands under ``"data"``. When the spawn ran isolated the node
|
|
526
|
-
also carries ``"isolated"`` and, when present, ``"patch_path"``,
|
|
527
|
-
``"branch_name"``, ``"nested_patches"``, ``"changes_applied"``
|
|
528
|
-
(``True``/``False``/``None`` — ``None`` means ``apply=False``), and
|
|
529
|
-
``"isolation_summary"``. If
|
|
530
|
-
the bridge returns no recoverable id the node still resolves with
|
|
531
|
-
``handle=None`` — the helper never throws.
|
|
496
|
+
"""Run a subagent and return its final output or structured data.
|
|
497
|
+
|
|
498
|
+
`schema` overrides agent and session schemas. `schema_mode` is
|
|
499
|
+
`"permissive"` or `"strict"`. `handle=True` returns the child output
|
|
500
|
+
reference and metadata, with parsed data under `"data"` when available.
|
|
532
501
|
"""
|
|
533
502
|
args = {"prompt": prompt}
|
|
534
503
|
if agent is not None:
|
|
@@ -539,6 +508,8 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
539
508
|
args["label"] = label
|
|
540
509
|
if schema is not None:
|
|
541
510
|
args["schema"] = schema
|
|
511
|
+
if schema_mode is not None:
|
|
512
|
+
args["schemaMode"] = schema_mode
|
|
542
513
|
if isolated is not None:
|
|
543
514
|
args["isolated"] = bool(isolated)
|
|
544
515
|
if apply is not None:
|
|
@@ -549,7 +520,8 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
549
520
|
args["handle"] = True
|
|
550
521
|
res = _bridge_call("__agent__", args)
|
|
551
522
|
text = res.get("text") if isinstance(res, dict) else res
|
|
552
|
-
|
|
523
|
+
has_data = isinstance(res, dict) and "data" in res
|
|
524
|
+
parsed = res["data"] if has_data else json.loads(text) if schema is not None else text
|
|
553
525
|
if not handle:
|
|
554
526
|
return parsed
|
|
555
527
|
details = res.get("details") if isinstance(res, dict) else None
|
|
@@ -568,7 +540,7 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
568
540
|
"id": details["id"],
|
|
569
541
|
"agent": details.get("agent"),
|
|
570
542
|
}
|
|
571
|
-
if schema is not None:
|
|
543
|
+
if has_data or schema is not None:
|
|
572
544
|
node["data"] = parsed
|
|
573
545
|
for src_key, dst_key in (
|
|
574
546
|
("isolated", "isolated"),
|
package/src/eval/rb/prelude.rb
CHANGED
|
@@ -392,22 +392,21 @@ unless defined?($__omp_prelude_loaded) && $__omp_prelude_loaded
|
|
|
392
392
|
schema.nil? ? text : JSON.parse(text)
|
|
393
393
|
end
|
|
394
394
|
|
|
395
|
-
def agent(prompt, agent: "task", model: nil, label: nil, schema: nil, isolated: nil, apply: nil, merge: nil, handle: false)
|
|
395
|
+
def agent(prompt, agent: "task", model: nil, label: nil, schema: nil, schema_mode: nil, isolated: nil, apply: nil, merge: nil, handle: false)
|
|
396
396
|
args = { "prompt" => prompt }
|
|
397
397
|
args["agent"] = agent unless agent.nil?
|
|
398
398
|
args["model"] = model unless model.nil?
|
|
399
399
|
args["label"] = label unless label.nil?
|
|
400
400
|
args["schema"] = schema unless schema.nil?
|
|
401
|
-
|
|
402
|
-
# with `apply`/`merge` controlling the post-run patch/branch merge.
|
|
401
|
+
args["schemaMode"] = schema_mode unless schema_mode.nil?
|
|
403
402
|
args["isolated"] = !!isolated unless isolated.nil?
|
|
404
403
|
args["apply"] = !!apply unless apply.nil?
|
|
405
404
|
args["merge"] = !!merge unless merge.nil?
|
|
406
|
-
# Tell the bridge a handle is wanted so it preserves the backing artifacts.
|
|
407
405
|
args["handle"] = true if handle
|
|
408
406
|
res = OmpBridge.call("__agent__", args)
|
|
409
407
|
text = res.is_a?(Hash) ? res["text"] : res
|
|
410
|
-
|
|
408
|
+
has_data = res.is_a?(Hash) && res.key?("data")
|
|
409
|
+
parsed = has_data ? res["data"] : (schema.nil? ? text : JSON.parse(text))
|
|
411
410
|
return parsed unless handle
|
|
412
411
|
details = res.is_a?(Hash) ? res["details"] : nil
|
|
413
412
|
if !details.is_a?(Hash) || details["id"].nil?
|
|
@@ -420,7 +419,7 @@ unless defined?($__omp_prelude_loaded) && $__omp_prelude_loaded
|
|
|
420
419
|
"id" => details["id"],
|
|
421
420
|
"agent" => details["agent"],
|
|
422
421
|
}
|
|
423
|
-
node["data"] = parsed
|
|
422
|
+
node["data"] = parsed if has_data || !schema.nil?
|
|
424
423
|
{
|
|
425
424
|
"isolated" => "isolated",
|
|
426
425
|
"patchPath" => "patch_path",
|
|
@@ -45,6 +45,8 @@ export interface BashResult {
|
|
|
45
45
|
output: string;
|
|
46
46
|
exitCode: number | undefined;
|
|
47
47
|
cancelled: boolean;
|
|
48
|
+
/** True when the command was killed by its timeout deadline (not a user abort). */
|
|
49
|
+
timedOut?: boolean;
|
|
48
50
|
truncated: boolean;
|
|
49
51
|
totalLines: number;
|
|
50
52
|
totalBytes: number;
|
|
@@ -70,6 +72,9 @@ const shellSessionsInUse = new Set<string>();
|
|
|
70
72
|
*/
|
|
71
73
|
const retainedShells = new Set<Shell>();
|
|
72
74
|
const RETAIN_REAP_INTERVAL_MS = 5_000;
|
|
75
|
+
// Native cancellation may spend two seconds unwinding the shell before its
|
|
76
|
+
// N-API chunk bridge drains. The JS watchdog must not race that teardown.
|
|
77
|
+
const NATIVE_TIMEOUT_FALLBACK_GRACE_MS = 5_000;
|
|
73
78
|
|
|
74
79
|
async function retainShellWithLiveBackgroundJobs(shell: Shell): Promise<void> {
|
|
75
80
|
let live: number;
|
|
@@ -306,16 +311,18 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
306
311
|
const nativeTimeoutMs = requestedTimeoutMs !== undefined && requestedTimeoutMs > 0 ? requestedTimeoutMs : undefined;
|
|
307
312
|
const nativeOwnsTimeout = nativeTimeoutMs !== undefined;
|
|
308
313
|
if (deadlineTimeoutMs !== undefined) {
|
|
314
|
+
const fallbackTimeoutMs = nativeOwnsTimeout
|
|
315
|
+
? deadlineTimeoutMs + NATIVE_TIMEOUT_FALLBACK_GRACE_MS
|
|
316
|
+
: deadlineTimeoutMs;
|
|
309
317
|
timeoutTimer = setTimeout(() => {
|
|
310
|
-
// Explicit timeouts are
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
// can terminate the Bun host before the native timeout result resolves.
|
|
318
|
+
// Explicit timeouts are enforced inside pi-natives via `timeoutMs`.
|
|
319
|
+
// Give native cancellation time to flush pipeline output and drain the
|
|
320
|
+
// N-API bridge before this result-only watchdog quarantines the run.
|
|
314
321
|
if (!nativeOwnsTimeout) {
|
|
315
322
|
abortCurrentExecution();
|
|
316
323
|
}
|
|
317
324
|
timeoutDeferred.resolve("timeout");
|
|
318
|
-
},
|
|
325
|
+
}, fallbackTimeoutMs);
|
|
319
326
|
}
|
|
320
327
|
|
|
321
328
|
let resetSession = false;
|
|
@@ -357,6 +364,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
357
364
|
return {
|
|
358
365
|
exitCode: undefined,
|
|
359
366
|
cancelled: true,
|
|
367
|
+
...(winner.kind === "timeout" ? { timedOut: true } : {}),
|
|
360
368
|
...(await sink.dump(
|
|
361
369
|
winner.kind === "timeout" && deadlineTimeoutMs !== undefined
|
|
362
370
|
? `Command timed out after ${Math.round(deadlineTimeoutMs / 1000)} seconds`
|
|
@@ -381,6 +389,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
381
389
|
return {
|
|
382
390
|
exitCode: undefined,
|
|
383
391
|
cancelled: true,
|
|
392
|
+
timedOut: true,
|
|
384
393
|
...(await sink.dump(annotation)),
|
|
385
394
|
};
|
|
386
395
|
}
|
|
@@ -18,7 +18,7 @@ import { getAllPluginToolPaths } from "../../extensibility/plugins/loader";
|
|
|
18
18
|
// Runtime self-reference: dereference this namespace only inside loader functions to keep the index.ts cycle safe.
|
|
19
19
|
import * as PiCodingAgent from "../../index";
|
|
20
20
|
import * as typebox from "../typebox";
|
|
21
|
-
import { createNoOpUIContext, resolvePath,
|
|
21
|
+
import { createNoOpUIContext, resolvePath, withHostGuard } from "../utils";
|
|
22
22
|
import type { CustomToolAPI, CustomToolFactory, LoadedCustomTool, ToolLoadError } from "./types";
|
|
23
23
|
|
|
24
24
|
interface LoadToolResult {
|
|
@@ -75,14 +75,14 @@ async function loadTool(
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
try {
|
|
78
|
-
const module = await
|
|
78
|
+
const module = await withHostGuard(() => import(resolvedPath));
|
|
79
79
|
const factory = (module.default ?? module) as CustomToolFactory;
|
|
80
80
|
|
|
81
81
|
if (typeof factory !== "function") {
|
|
82
82
|
return { tools: [], errors: [{ path: toolPath, error: "Tool must export a default function", source }] };
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
const toolResult: unknown = await
|
|
85
|
+
const toolResult: unknown = await withHostGuard(async () => factory(sharedApi));
|
|
86
86
|
const toolsArray = Array.isArray(toolResult) ? toolResult : [toolResult];
|
|
87
87
|
|
|
88
88
|
const loadedTools: LoadedCustomTool[] = [];
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import type { AgentTool, AgentToolUpdateCallback, ToolLoadMode } from "@oh-my-pi/pi-agent-core";
|
|
5
5
|
import type { Static, TSchema } from "@oh-my-pi/pi-ai";
|
|
6
6
|
import type { Theme } from "../../modes/theme/theme";
|
|
7
|
+
import { defaultLoadModeForToolName } from "../../tools/essential-tools";
|
|
7
8
|
import { applyToolProxy } from "../tool-proxy";
|
|
8
9
|
import type { CustomTool, CustomToolContext } from "./types";
|
|
9
10
|
|
|
@@ -23,7 +24,7 @@ export class CustomToolAdapter<TParams extends TSchema = TSchema, TDetails = any
|
|
|
23
24
|
) {
|
|
24
25
|
applyToolProxy(tool, this);
|
|
25
26
|
this.strict = tool.strict;
|
|
26
|
-
this.loadMode = tool.loadMode
|
|
27
|
+
this.loadMode = defaultLoadModeForToolName(tool.name, tool.loadMode);
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
execute(
|
|
@@ -24,7 +24,7 @@ import { installLegacyPiSpecifierShim, loadLegacyPiModule } from "../plugins/leg
|
|
|
24
24
|
import { getAllPluginExtensionPaths } from "../plugins/loader";
|
|
25
25
|
import * as TypeBox from "../typebox";
|
|
26
26
|
|
|
27
|
-
import { resolvePath,
|
|
27
|
+
import { resolvePath, withHostGuard } from "../utils";
|
|
28
28
|
import type {
|
|
29
29
|
AssistantThinkingRenderer,
|
|
30
30
|
Extension,
|
|
@@ -290,7 +290,7 @@ async function loadExtension(
|
|
|
290
290
|
): Promise<{ extension: Extension | null; error: string | null }> {
|
|
291
291
|
const resolvedPath = resolvePath(extensionPath, cwd);
|
|
292
292
|
try {
|
|
293
|
-
const module = (await
|
|
293
|
+
const module = (await withHostGuard(() => loadLegacyPiModule(resolvedPath))) as LoadedExtensionModule;
|
|
294
294
|
const factory = getExtensionFactory(module);
|
|
295
295
|
|
|
296
296
|
if (typeof factory !== "function") {
|
|
@@ -302,7 +302,7 @@ async function loadExtension(
|
|
|
302
302
|
|
|
303
303
|
const extension = createExtension(extensionPath, resolvedPath);
|
|
304
304
|
const api = new ConcreteExtensionAPI(PiCodingAgent, extension, runtime, cwd, eventBus);
|
|
305
|
-
await
|
|
305
|
+
await withHostGuard(async () => {
|
|
306
306
|
await factory(api);
|
|
307
307
|
});
|
|
308
308
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managed timers for extensions.
|
|
3
|
+
*
|
|
4
|
+
* Extensions scheduling their own background work through raw `setInterval` /
|
|
5
|
+
* `setTimeout` used to be able to take down the whole session: a throw inside
|
|
6
|
+
* the callback runs on a fresh stack outside the handler-dispatch try/catch,
|
|
7
|
+
* surfaces as a process-level `uncaughtException`, and the global postmortem
|
|
8
|
+
* handler treats that as fatal (issue #5664).
|
|
9
|
+
*
|
|
10
|
+
* {@link ManagedTimers} backs the sanctioned `ctx.setInterval` /
|
|
11
|
+
* `ctx.setTimeout` helpers. Each callback runs inside the same isolation the
|
|
12
|
+
* runner already applies to handler dispatch — a synchronous throw or a
|
|
13
|
+
* rejected promise is reported through `onError` and swallowed — and every
|
|
14
|
+
* outstanding handle is `unref`'d (never keeps the process alive) and cleared
|
|
15
|
+
* on session teardown via {@link clearAll}.
|
|
16
|
+
*/
|
|
17
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
18
|
+
|
|
19
|
+
/** Callback invoked when a managed timer's callback throws or rejects. */
|
|
20
|
+
export type ManagedTimerErrorHandler = (event: string, error: string, stack?: string) => void;
|
|
21
|
+
|
|
22
|
+
export class ManagedTimers {
|
|
23
|
+
readonly #timers = new Set<Timer>();
|
|
24
|
+
|
|
25
|
+
constructor(private readonly onError: ManagedTimerErrorHandler) {}
|
|
26
|
+
|
|
27
|
+
/** Schedule a repeating callback whose throws are contained. */
|
|
28
|
+
setInterval(callback: (...args: unknown[]) => void, ms?: number, ...args: unknown[]): Timer {
|
|
29
|
+
const timer = setInterval(() => this.#run("interval", callback, args), ms, ...args);
|
|
30
|
+
timer.unref?.();
|
|
31
|
+
this.#timers.add(timer);
|
|
32
|
+
return timer;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Schedule a one-shot callback whose throws are contained. Deregisters after it fires. */
|
|
36
|
+
setTimeout(callback: (...args: unknown[]) => void, ms?: number, ...args: unknown[]): Timer {
|
|
37
|
+
const timer = setTimeout(
|
|
38
|
+
() => {
|
|
39
|
+
this.#timers.delete(timer);
|
|
40
|
+
this.#run("timeout", callback, args);
|
|
41
|
+
},
|
|
42
|
+
ms,
|
|
43
|
+
...args,
|
|
44
|
+
);
|
|
45
|
+
timer.unref?.();
|
|
46
|
+
this.#timers.add(timer);
|
|
47
|
+
return timer;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Clear one managed timer. Accepts an interval or timeout handle. */
|
|
51
|
+
clear(timer: Timer): void {
|
|
52
|
+
if (!this.#timers.delete(timer)) return;
|
|
53
|
+
clearInterval(timer);
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Clear every outstanding managed timer. Called on session teardown. */
|
|
58
|
+
clearAll(): void {
|
|
59
|
+
for (const timer of this.#timers) {
|
|
60
|
+
clearInterval(timer);
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
}
|
|
63
|
+
this.#timers.clear();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#run(kind: "interval" | "timeout", callback: (...args: unknown[]) => void, args: unknown[]): void {
|
|
67
|
+
try {
|
|
68
|
+
const result = callback(...args) as unknown;
|
|
69
|
+
if (result instanceof Promise) {
|
|
70
|
+
result.catch((err: unknown) => this.#report(kind, err));
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
this.#report(kind, err);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#report(kind: "interval" | "timeout", err: unknown): void {
|
|
78
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
79
|
+
const stack = err instanceof Error ? err.stack : undefined;
|
|
80
|
+
logger.warn("Extension timer callback threw", { event: `${kind}_callback`, error: message });
|
|
81
|
+
this.onError(`${kind}_callback`, message, stack);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -12,6 +12,7 @@ import type { MemoryRuntimeContext } from "../../memory-backend";
|
|
|
12
12
|
import { type Theme, theme } from "../../modes/theme/theme";
|
|
13
13
|
import type { SessionManager } from "../../session/session-manager";
|
|
14
14
|
import type { BranchHandler, NavigateTreeHandler, NewSessionHandler } from "../session-handler-types";
|
|
15
|
+
import { ManagedTimers } from "./managed-timers";
|
|
15
16
|
import { createExtensionModelQuery } from "./model-api";
|
|
16
17
|
import type {
|
|
17
18
|
AfterProviderResponseEvent,
|
|
@@ -249,6 +250,18 @@ export class ExtensionRunner {
|
|
|
249
250
|
*/
|
|
250
251
|
#pendingCredentialDisabled: CredentialDisabledEvent[] = [];
|
|
251
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Timers scheduled by extensions through the sanctioned `ctx.setInterval` /
|
|
255
|
+
* `ctx.setTimeout` helpers. Callbacks run with the same isolation as handler
|
|
256
|
+
* dispatch — a throw is logged and routed through {@link onError} instead of
|
|
257
|
+
* escaping to the process `uncaughtException` handler and tearing down the
|
|
258
|
+
* whole session (issue #5664). Handles are `unref`'d and every outstanding
|
|
259
|
+
* timer is cleared on session teardown via {@link clearManagedTimers}.
|
|
260
|
+
*/
|
|
261
|
+
#managedTimers = new ManagedTimers((event, error, stack) =>
|
|
262
|
+
this.emitError({ extensionPath: "<timer>", event, error, stack }),
|
|
263
|
+
);
|
|
264
|
+
|
|
252
265
|
constructor(
|
|
253
266
|
private readonly extensions: Extension[],
|
|
254
267
|
private readonly runtime: ExtensionRuntime,
|
|
@@ -542,6 +555,9 @@ export class ExtensionRunner {
|
|
|
542
555
|
getSystemPrompt: () => this.#getSystemPromptFn(),
|
|
543
556
|
localProtocolOptions: this.localProtocolOptions,
|
|
544
557
|
memory: this.#getMemoryFn?.(),
|
|
558
|
+
setInterval: (callback, ms, ...args) => this.#managedTimers.setInterval(callback, ms, ...args),
|
|
559
|
+
setTimeout: (callback, ms, ...args) => this.#managedTimers.setTimeout(callback, ms, ...args),
|
|
560
|
+
clearTimer: timer => this.#managedTimers.clear(timer),
|
|
545
561
|
};
|
|
546
562
|
}
|
|
547
563
|
|
|
@@ -552,6 +568,16 @@ export class ExtensionRunner {
|
|
|
552
568
|
this.#shutdownHandler();
|
|
553
569
|
}
|
|
554
570
|
|
|
571
|
+
/**
|
|
572
|
+
* Clear every timer scheduled through `ctx.setInterval` / `ctx.setTimeout`.
|
|
573
|
+
* Called during session teardown so extension background work does not
|
|
574
|
+
* outlive the session (a self-scheduling interval would otherwise keep
|
|
575
|
+
* firing against a disposed session).
|
|
576
|
+
*/
|
|
577
|
+
clearManagedTimers(): void {
|
|
578
|
+
this.#managedTimers.clearAll();
|
|
579
|
+
}
|
|
580
|
+
|
|
555
581
|
createCommandContext(): ExtensionCommandContext {
|
|
556
582
|
return {
|
|
557
583
|
...this.createContext(),
|
|
@@ -440,6 +440,24 @@ export interface ExtensionContext {
|
|
|
440
440
|
getSystemPrompt(): string[];
|
|
441
441
|
/** Structured memory runtime for status/search/save across the configured backend. */
|
|
442
442
|
memory?: MemoryRuntimeContext;
|
|
443
|
+
/**
|
|
444
|
+
* Schedule a repeating callback whose throws are contained. Unlike raw
|
|
445
|
+
* `setInterval`, a synchronous throw or rejected promise from `callback` is
|
|
446
|
+
* logged and surfaced through the extension error channel instead of
|
|
447
|
+
* escaping as a process-fatal `uncaughtException` — one misbehaving timer
|
|
448
|
+
* can no longer take down the whole session. The handle is `unref`'d and
|
|
449
|
+
* cleared automatically on `session_shutdown`. Prefer this over raw
|
|
450
|
+
* `setInterval` for any extension background work.
|
|
451
|
+
*/
|
|
452
|
+
setInterval(callback: (...args: unknown[]) => void, ms?: number, ...args: unknown[]): Timer;
|
|
453
|
+
/**
|
|
454
|
+
* Schedule a one-shot callback whose throws are contained, mirroring
|
|
455
|
+
* {@link setInterval}. Cleared automatically on `session_shutdown` if it has
|
|
456
|
+
* not yet fired.
|
|
457
|
+
*/
|
|
458
|
+
setTimeout(callback: (...args: unknown[]) => void, ms?: number, ...args: unknown[]): Timer;
|
|
459
|
+
/** Clear a timer scheduled via {@link setInterval} or {@link setTimeout}. */
|
|
460
|
+
clearTimer(timer: Timer): void;
|
|
443
461
|
}
|
|
444
462
|
|
|
445
463
|
/**
|
|
@@ -12,6 +12,7 @@ import type { ImageContent, Static, TextContent, TSchema } from "@oh-my-pi/pi-ai
|
|
|
12
12
|
import type { Settings } from "../../config/settings";
|
|
13
13
|
import type { Theme } from "../../modes/theme/theme";
|
|
14
14
|
import { type ApprovalMode, formatApprovalPrompt, resolveApproval } from "../../tools/approval";
|
|
15
|
+
import { defaultLoadModeForToolName } from "../../tools/essential-tools";
|
|
15
16
|
import { normalizeToolEventInput, resolveToolEventInput } from "../tool-event-input";
|
|
16
17
|
import { applyToolProxy } from "../tool-proxy";
|
|
17
18
|
import type { ExtensionRunner } from "./runner";
|
|
@@ -36,7 +37,7 @@ export class RegisteredToolAdapter implements AgentTool<any, any, any> {
|
|
|
36
37
|
private runner: ExtensionRunner,
|
|
37
38
|
) {
|
|
38
39
|
applyToolProxy(registeredTool.definition, this);
|
|
39
|
-
this.loadMode = registeredTool.definition.loadMode
|
|
40
|
+
this.loadMode = defaultLoadModeForToolName(registeredTool.definition.name, registeredTool.definition.loadMode);
|
|
40
41
|
|
|
41
42
|
// Only define render methods when the underlying definition provides them.
|
|
42
43
|
// If these exist unconditionally on the prototype, ToolExecutionComponent
|
|
@@ -12,7 +12,7 @@ import { loadCapability } from "../../discovery";
|
|
|
12
12
|
import * as PiCodingAgent from "../../index";
|
|
13
13
|
import type { CustomMessagePayload } from "../../session/messages";
|
|
14
14
|
import * as typebox from "../typebox";
|
|
15
|
-
import { resolvePath,
|
|
15
|
+
import { resolvePath, withHostGuard } from "../utils";
|
|
16
16
|
import { execCommand } from "./runner";
|
|
17
17
|
import type { ExecOptions, HookAPI, HookFactory, HookMessageRenderer, RegisteredCommand } from "./types";
|
|
18
18
|
|
|
@@ -149,7 +149,7 @@ async function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHo
|
|
|
149
149
|
|
|
150
150
|
try {
|
|
151
151
|
// Import the module using native Bun import
|
|
152
|
-
const module = await
|
|
152
|
+
const module = await withHostGuard(() => import(resolvedPath));
|
|
153
153
|
const factory = module.default as HookFactory;
|
|
154
154
|
|
|
155
155
|
if (typeof factory !== "function") {
|
|
@@ -164,7 +164,7 @@ async function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHo
|
|
|
164
164
|
);
|
|
165
165
|
|
|
166
166
|
// Call factory to register handlers
|
|
167
|
-
await
|
|
167
|
+
await withHostGuard(async () => factory(api));
|
|
168
168
|
|
|
169
169
|
return {
|
|
170
170
|
hook: {
|