@oh-my-pi/pi-coding-agent 15.10.11 → 15.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -2
- package/dist/cli.js +5790 -5731
- package/dist/types/async/index.d.ts +0 -1
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/gallery-fixtures/types.d.ts +5 -0
- package/dist/types/cli-commands.d.ts +12 -0
- package/dist/types/commands/launch.d.ts +4 -0
- package/dist/types/config/api-key-resolver.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +6 -1
- package/dist/types/config/model-registry.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +18 -0
- package/dist/types/config/settings-schema.d.ts +85 -34
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
- package/dist/types/eval/py/executor.d.ts +5 -0
- package/dist/types/eval/py/kernel.d.ts +6 -1
- package/dist/types/eval/py/runtime.d.ts +9 -0
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/extensions/runner.d.ts +3 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/internal-urls/history-protocol.d.ts +14 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/irc/bus.d.ts +66 -0
- package/dist/types/memory-backend/index.d.ts +1 -0
- package/dist/types/memory-backend/runtime.d.ts +4 -0
- package/dist/types/memory-backend/types.d.ts +66 -1
- package/dist/types/modes/components/agent-hub.d.ts +30 -0
- package/dist/types/modes/components/compaction-summary-message.d.ts +10 -4
- package/dist/types/modes/components/custom-editor.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
- package/dist/types/modes/components/welcome.d.ts +3 -9
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/index.d.ts +3 -3
- package/dist/types/modes/interactive-mode.d.ts +10 -4
- package/dist/types/modes/oauth-manual-input.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +39 -2
- package/dist/types/modes/rpc/rpc-mode.d.ts +31 -2
- package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +75 -1
- package/dist/types/modes/setup-wizard/index.d.ts +5 -1
- package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +5 -2
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
- package/dist/types/registry/agent-lifecycle.d.ts +51 -0
- package/dist/types/registry/agent-registry.d.ts +16 -5
- package/dist/types/secrets/index.d.ts +1 -1
- package/dist/types/secrets/obfuscator.d.ts +8 -2
- package/dist/types/session/agent-session.d.ts +49 -32
- package/dist/types/session/messages.d.ts +2 -4
- package/dist/types/session/session-history-format.d.ts +12 -0
- package/dist/types/session/session-manager.d.ts +21 -3
- package/dist/types/session/streaming-output.d.ts +46 -0
- package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
- package/dist/types/slash-commands/types.d.ts +1 -1
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +12 -2
- package/dist/types/task/index.d.ts +13 -6
- package/dist/types/task/output-manager.d.ts +0 -7
- package/dist/types/task/repair-args.d.ts +8 -7
- package/dist/types/task/types.d.ts +63 -51
- package/dist/types/thinking.d.ts +4 -0
- package/dist/types/tiny/title-client.d.ts +11 -0
- package/dist/types/tiny/title-protocol.d.ts +1 -0
- package/dist/types/tools/browser/tab-worker.d.ts +3 -1
- package/dist/types/tools/find.d.ts +0 -11
- package/dist/types/tools/grouped-file-output.d.ts +0 -49
- package/dist/types/tools/index.d.ts +7 -3
- package/dist/types/tools/irc.d.ts +76 -38
- package/dist/types/tools/job.d.ts +7 -1
- package/dist/types/utils/git.d.ts +15 -2
- package/dist/types/utils/title-generator.d.ts +3 -2
- package/examples/extensions/with-deps/package.json +1 -0
- package/package.json +11 -10
- package/scripts/bundle-dist.ts +28 -19
- package/src/async/index.ts +0 -1
- package/src/auto-thinking/classifier.ts +1 -0
- package/src/cli/args.ts +3 -0
- package/src/cli/gallery-cli.ts +1 -1
- package/src/cli/gallery-fixtures/agentic.ts +230 -115
- package/src/cli/gallery-fixtures/types.ts +5 -0
- package/src/cli-commands.ts +29 -0
- package/src/cli.ts +28 -15
- package/src/commands/launch.ts +4 -0
- package/src/commit/agentic/tools/analyze-file.ts +38 -19
- package/src/commit/model-selection.ts +3 -2
- package/src/config/api-key-resolver.ts +8 -6
- package/src/config/keybindings.ts +6 -1
- package/src/config/model-registry.ts +97 -30
- package/src/config/model-resolver.ts +60 -0
- package/src/config/settings-schema.ts +99 -55
- package/src/config/settings.ts +68 -3
- package/src/edit/hashline/execute.ts +39 -2
- package/src/edit/hashline/noop-loop-guard.ts +99 -0
- package/src/eval/__tests__/agent-bridge.test.ts +5 -3
- package/src/eval/agent-bridge.ts +3 -16
- package/src/eval/completion-bridge.ts +1 -0
- package/src/eval/js/shared/prelude.txt +1 -1
- package/src/eval/py/executor.ts +29 -7
- package/src/eval/py/index.ts +6 -1
- package/src/eval/py/kernel.ts +31 -11
- package/src/eval/py/prelude.py +5 -6
- package/src/eval/py/runtime.ts +37 -0
- package/src/exec/bash-executor.ts +82 -3
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +38 -13
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/extensions/get-commands-handler.ts +2 -1
- package/src/extensibility/extensions/runner.ts +6 -1
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/shared-events.ts +2 -2
- package/src/hindsight/bank.ts +17 -2
- package/src/internal-urls/docs-index.generated.ts +11 -11
- package/src/internal-urls/history-protocol.ts +113 -0
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/router.ts +3 -1
- package/src/internal-urls/types.ts +1 -1
- package/src/irc/bus.ts +292 -0
- package/src/main.ts +26 -66
- package/src/memories/index.ts +2 -0
- package/src/memory-backend/index.ts +1 -0
- package/src/memory-backend/local-backend.ts +9 -0
- package/src/memory-backend/off-backend.ts +9 -0
- package/src/memory-backend/runtime.ts +66 -0
- package/src/memory-backend/types.ts +81 -1
- package/src/mnemopi/backend.ts +151 -4
- package/src/modes/acp/acp-agent.ts +119 -11
- package/src/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
- package/src/modes/components/assistant-message.ts +19 -21
- package/src/modes/components/compaction-summary-message.ts +68 -32
- package/src/modes/components/custom-editor.ts +10 -0
- package/src/modes/components/footer.ts +3 -1
- package/src/modes/components/status-line/component.ts +118 -34
- package/src/modes/components/tool-execution.ts +31 -1
- package/src/modes/components/ttsr-notification.ts +72 -30
- package/src/modes/components/welcome.ts +9 -33
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +65 -0
- package/src/modes/controllers/extension-ui-controller.ts +8 -8
- package/src/modes/controllers/input-controller.ts +19 -2
- package/src/modes/controllers/mcp-command-controller.ts +38 -3
- package/src/modes/controllers/selector-controller.ts +21 -17
- package/src/modes/index.ts +3 -21
- package/src/modes/interactive-mode.ts +47 -22
- package/src/modes/oauth-manual-input.ts +30 -3
- package/src/modes/rpc/rpc-client.ts +154 -3
- package/src/modes/rpc/rpc-mode.ts +97 -12
- package/src/modes/rpc/rpc-subagents.ts +265 -0
- package/src/modes/rpc/rpc-types.ts +81 -1
- package/src/modes/setup-wizard/index.ts +12 -2
- package/src/modes/setup-wizard/lazy.ts +16 -0
- package/src/modes/theme/theme.ts +18 -5
- package/src/modes/types.ts +5 -5
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +51 -49
- package/src/prompts/system/irc-incoming.md +3 -4
- package/src/prompts/system/orchestrate-notice.md +2 -2
- package/src/prompts/system/subagent-system-prompt.md +0 -5
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/system/workflow-notice.md +2 -2
- package/src/prompts/tools/eval.md +3 -3
- package/src/prompts/tools/irc.md +29 -19
- package/src/prompts/tools/read.md +2 -2
- package/src/prompts/tools/task-summary.md +5 -16
- package/src/prompts/tools/task.md +38 -29
- package/src/registry/agent-lifecycle.ts +218 -0
- package/src/registry/agent-registry.ts +16 -5
- package/src/sdk.ts +37 -10
- package/src/secrets/index.ts +8 -1
- package/src/secrets/obfuscator.ts +39 -18
- package/src/session/agent-session.ts +422 -291
- package/src/session/messages.ts +11 -78
- package/src/session/session-history-format.ts +246 -0
- package/src/session/session-manager.ts +59 -5
- package/src/session/streaming-output.ts +226 -10
- package/src/slash-commands/acp-builtins.ts +24 -0
- package/src/slash-commands/builtin-registry.ts +20 -0
- package/src/slash-commands/types.ts +1 -1
- package/src/system-prompt.ts +14 -0
- package/src/task/executor.ts +851 -461
- package/src/task/index.ts +721 -796
- package/src/task/output-manager.ts +0 -11
- package/src/task/render.ts +148 -63
- package/src/task/repair-args.ts +21 -9
- package/src/task/types.ts +82 -66
- package/src/thinking.ts +7 -0
- package/src/tiny/title-client.ts +34 -5
- package/src/tiny/title-protocol.ts +1 -1
- package/src/tiny/worker.ts +6 -4
- package/src/tools/ask.ts +4 -2
- package/src/tools/bash.ts +61 -10
- package/src/tools/browser/tab-worker.ts +26 -7
- package/src/tools/browser.ts +28 -1
- package/src/tools/find.ts +2 -27
- package/src/tools/grouped-file-output.ts +1 -118
- package/src/tools/image-gen.ts +11 -4
- package/src/tools/index.ts +17 -13
- package/src/tools/inspect-image.ts +1 -0
- package/src/tools/irc.ts +596 -171
- package/src/tools/job.ts +41 -7
- package/src/tools/read.ts +57 -1
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +4 -1
- package/src/utils/commit-message-generator.ts +1 -0
- package/src/utils/git.ts +267 -13
- package/src/utils/title-generator.ts +24 -5
- package/dist/types/async/support.d.ts +0 -2
- package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
- package/dist/types/task/simple-mode.d.ts +0 -8
- package/src/async/support.ts +0 -5
- package/src/task/simple-mode.ts +0 -27
|
@@ -1,43 +1,51 @@
|
|
|
1
|
-
|
|
1
|
+
{{#if batchEnabled}}Spawns subagents to work in the background — one per `tasks[]` item; a single spawn is a one-item batch.{{else}}Spawns ONE subagent per call to work in the background.{{/if}}
|
|
2
2
|
|
|
3
|
-
{{#if
|
|
4
|
-
-
|
|
5
|
-
-
|
|
3
|
+
- Spawning is non-blocking: the call returns immediately with the agent id{{#if batchEnabled}}s{{/if}} and job id{{#if batchEnabled}}s{{/if}}; each result is delivered automatically when that agent yields.
|
|
4
|
+
- Parallelism = {{#if batchEnabled}}`tasks[]` items in one call, and/or multiple `task` calls in one assistant message{{else}}multiple `task` calls in one assistant message{{/if}}. Concurrency is bounded at {{MAX_CONCURRENCY}} running subagents per session.
|
|
5
|
+
- If genuinely blocked on a result, wait with `job poll`; otherwise keep working. `job cancel` terminates a task and **cannot carry a message** — only for stalled/abandoned work.
|
|
6
6
|
{{#if ircEnabled}}
|
|
7
|
-
- Coordinate with running
|
|
8
|
-
- If genuinely blocked on completion, wait with `job poll`; otherwise keep working.
|
|
9
|
-
{{else}}
|
|
10
|
-
- If genuinely blocked on completion, wait with `job poll`; otherwise keep working.
|
|
11
|
-
- Use `job list` to snapshot manager state; `cancel: [id]` only to actually stop a stuck task.
|
|
12
|
-
{{/if}}
|
|
7
|
+
- Coordinate with running agents via `irc` using their ids. Agents reach you and their siblings live the same way.
|
|
13
8
|
{{/if}}
|
|
14
9
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
{{/if}}
|
|
10
|
+
<lifecycle>
|
|
11
|
+
- Finished agents stay alive: `idle` first, then `parked` after a TTL.{{#if ircEnabled}} Both remain addressable and revivable: messaging one via `irc` wakes it and runs your message as a follow-up turn. **Prefer messaging an agent that already holds the relevant context over spawning fresh** — check `irc` op:"list" for candidates.{{/if}}
|
|
12
|
+
- `history://<id>` is the agent's transcript; `agent://<id>` its latest output artifact.
|
|
13
|
+
</lifecycle>
|
|
20
14
|
|
|
21
15
|
<parameters>
|
|
22
|
-
- `agent`: agent type
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
{{#if isolationEnabled}}
|
|
16
|
+
- `agent`: agent type to spawn
|
|
17
|
+
{{#if batchEnabled}}
|
|
18
|
+
- `context`: shared background prepended to every assignment — goal, constraints, shared contract (see context-fmt); REQUIRED, session-specific only
|
|
19
|
+
- `tasks`: tasks to spawn — one subagent per item, all in parallel:
|
|
20
|
+
- `assignment`: complete self-contained instructions; one-liners and missing acceptance criteria are PROHIBITED
|
|
21
|
+
- `id`: stable agent id, CamelCase, ≤32 chars; generated when omitted
|
|
22
|
+
- `description`: UI label only — subagent never sees it
|
|
23
|
+
{{#if isolationEnabled}}
|
|
24
|
+
- `isolated`: run this spawn in an isolated env; returns patches. Isolated agents are torn down at completion — not addressable afterwards
|
|
25
|
+
{{/if}}
|
|
26
|
+
{{else}}
|
|
27
|
+
- `id`: stable agent id, CamelCase, ≤32 chars; generated when omitted
|
|
28
|
+
- `description`: UI label only — subagent never sees it
|
|
29
|
+
- `assignment`: complete self-contained instructions; one-liners and missing acceptance criteria are PROHIBITED
|
|
30
|
+
{{#if isolationEnabled}}
|
|
31
|
+
- `isolated`: run in isolated env; returns patches. Isolated agents are torn down at completion — not addressable afterwards
|
|
32
|
+
{{/if}}
|
|
33
|
+
{{/if}}
|
|
30
34
|
</parameters>
|
|
31
35
|
|
|
32
36
|
<rules>
|
|
33
|
-
- **Maximize
|
|
34
|
-
- **Subagents do not verify, lint, or format.** Every assignment MUST instruct the subagent to skip all gates, formatters, and project-wide build/test/lint. You run them once at the end across the union of changed files
|
|
37
|
+
- **Maximize fan-out.** Issue the widest {{#if batchEnabled}}`tasks[]` batch (or set of parallel `task` calls){{else}}set of parallel `task` calls{{/if}} the work decomposes into. NEVER serialize work that could run concurrently.
|
|
38
|
+
- **Subagents do not verify, lint, or format.** Every assignment MUST instruct the subagent to skip all gates, formatters, and project-wide build/test/lint. You run them once at the end across the union of changed files.
|
|
35
39
|
- No globs, no "update all", no package-wide scope. Fan out.
|
|
36
40
|
- NEVER slow down or serialize because tasks might overlap on some files. Agents resolve collisions among themselves in real time.
|
|
37
|
-
-
|
|
38
|
-
{{#if
|
|
41
|
+
- Subagents have no conversation history. Every fact, file path, and direction they need MUST be explicit in {{#if batchEnabled}}`context` or the item's `assignment`{{else}}the `assignment`{{/if}}.
|
|
42
|
+
{{#if batchEnabled}}
|
|
43
|
+
- **Shared background** lives in `context` once — never duplicated across assignments. Pass large payloads via `local://<path>` URIs, not inline.
|
|
44
|
+
{{else}}
|
|
45
|
+
- **Shared background**: write it ONCE to a `local://` file (e.g. `local://ctx.md`) and reference that path in each assignment. Pass large payloads via `local://<path>` URIs, not inline.
|
|
46
|
+
{{/if}}
|
|
39
47
|
- Prefer agents that investigate **and** edit in one pass; only spin a read-only discovery step when affected files are genuinely unknown.
|
|
40
|
-
- **Read-only agents**: Agents tagged READ-ONLY (e.g. `explore`) have no edit/write/command tools. NEVER hand them an assignment that requires changing files or running commands
|
|
48
|
+
- **Read-only agents**: Agents tagged READ-ONLY (e.g. `explore`) have no edit/write/command tools. NEVER hand them an assignment that requires changing files or running commands. Use them to investigate and report back; do the edits yourself or delegate to a writing agent (`task`, `oracle`, `designer`).
|
|
41
49
|
- **No reasoning offload**: NEVER offload reasoning, analysis, design, or decision-making to `quick_task` or `explore` — they run minimal-effort / small models for mechanical lookups and data collection only. Keep judgment and synthesis in your own context; delegate hard thinking to `task`, `plan`, or `oracle`.
|
|
42
50
|
</rules>
|
|
43
51
|
|
|
@@ -51,9 +59,10 @@ Test: can task B run correctly without seeing A's output? If no, sequence A →
|
|
|
51
59
|
Sequential when one task produces a contract (types, API, schema, core module) the other consumes.
|
|
52
60
|
Parallel when tasks touch disjoint files or are independent refactors/tests.
|
|
53
61
|
{{/if}}
|
|
62
|
+
{{#if ircEnabled}}Sequenced follow-ups SHOULD message the agent that produced the prerequisite — it already holds the context.{{/if}}
|
|
54
63
|
</parallelization>
|
|
55
64
|
|
|
56
|
-
{{#if
|
|
65
|
+
{{#if batchEnabled}}
|
|
57
66
|
<context-fmt>
|
|
58
67
|
# Goal ← one sentence: what the batch accomplishes
|
|
59
68
|
# Constraints ← MUST/NEVER rules and session decisions
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentLifecycleManager - Owns the idle → parked → revived lifecycle of
|
|
3
|
+
* adopted subagents.
|
|
4
|
+
*
|
|
5
|
+
* The task executor hands a finished agent over via {@link AgentLifecycleManager.adopt};
|
|
6
|
+
* from then on the manager arms a TTL timer whenever the agent goes `idle`,
|
|
7
|
+
* parks it on expiry (disposes the live session, keeps the AgentRef +
|
|
8
|
+
* sessionFile), and revives it on demand through
|
|
9
|
+
* {@link AgentLifecycleManager.ensureLive}. Only this manager flips
|
|
10
|
+
* `parked` ↔ `idle`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
14
|
+
import type { AgentSession } from "../session/agent-session";
|
|
15
|
+
import { AgentRegistry, MAIN_AGENT_ID, type RegistryEvent } from "./agent-registry";
|
|
16
|
+
|
|
17
|
+
export type AgentReviver = () => Promise<AgentSession>;
|
|
18
|
+
|
|
19
|
+
export interface AdoptOptions {
|
|
20
|
+
/** TTL before an idle agent is parked. <= 0 disables parking. */
|
|
21
|
+
idleTtlMs: number;
|
|
22
|
+
/** Recreates a live AgentSession from the ref's sessionFile. Absent => not resumable after park (e.g. isolated runs). */
|
|
23
|
+
revive?: AgentReviver;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface AdoptedAgent {
|
|
27
|
+
idleTtlMs: number;
|
|
28
|
+
revive?: AgentReviver;
|
|
29
|
+
timer?: NodeJS.Timeout;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class AgentLifecycleManager {
|
|
33
|
+
static #global: AgentLifecycleManager | undefined;
|
|
34
|
+
|
|
35
|
+
static global(): AgentLifecycleManager {
|
|
36
|
+
if (!AgentLifecycleManager.#global) {
|
|
37
|
+
AgentLifecycleManager.#global = new AgentLifecycleManager();
|
|
38
|
+
}
|
|
39
|
+
return AgentLifecycleManager.#global;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Reset the global manager. Test-only. */
|
|
43
|
+
static resetGlobalForTests(): void {
|
|
44
|
+
const current = AgentLifecycleManager.#global;
|
|
45
|
+
if (current) {
|
|
46
|
+
current.#unsubscribe?.();
|
|
47
|
+
current.#unsubscribe = undefined;
|
|
48
|
+
for (const adopted of current.#adopted.values()) {
|
|
49
|
+
clearTimeout(adopted.timer);
|
|
50
|
+
}
|
|
51
|
+
current.#adopted.clear();
|
|
52
|
+
current.#revivals.clear();
|
|
53
|
+
current.#parking.clear();
|
|
54
|
+
}
|
|
55
|
+
AgentLifecycleManager.#global = undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
readonly #registry: AgentRegistry;
|
|
59
|
+
readonly #adopted = new Map<string, AdoptedAgent>();
|
|
60
|
+
/** Ids whose session is being disposed by {@link park} right now. */
|
|
61
|
+
readonly #parking = new Set<string>();
|
|
62
|
+
/** In-flight revives, so concurrent {@link ensureLive} calls coalesce. */
|
|
63
|
+
readonly #revivals = new Map<string, Promise<AgentSession>>();
|
|
64
|
+
#unsubscribe: (() => void) | undefined;
|
|
65
|
+
|
|
66
|
+
constructor(registry: AgentRegistry = AgentRegistry.global()) {
|
|
67
|
+
this.#registry = registry;
|
|
68
|
+
this.#unsubscribe = registry.onChange(event => this.#onRegistryEvent(event));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Take ownership of a finished subagent. Caller has already set registry
|
|
73
|
+
* status to "idle". Arms the TTL timer (idleTtlMs <= 0 adopts without one).
|
|
74
|
+
*/
|
|
75
|
+
adopt(id: string, opts: AdoptOptions): void {
|
|
76
|
+
if (id === MAIN_AGENT_ID) return;
|
|
77
|
+
if (!this.#registry.get(id)) {
|
|
78
|
+
logger.warn("AgentLifecycleManager.adopt: unknown agent id", { id });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const existing = this.#adopted.get(id);
|
|
82
|
+
clearTimeout(existing?.timer);
|
|
83
|
+
const adopted: AdoptedAgent = { idleTtlMs: opts.idleTtlMs, revive: opts.revive };
|
|
84
|
+
this.#adopted.set(id, adopted);
|
|
85
|
+
this.#armTimer(id, adopted);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** True if the id is adopted (parked or live). */
|
|
89
|
+
has(id: string): boolean {
|
|
90
|
+
return this.#adopted.has(id);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** True while {@link park} is disposing this agent's session (lets dispose hooks distinguish park from teardown). */
|
|
94
|
+
isParking(id: string): boolean {
|
|
95
|
+
return this.#parking.has(id);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Dispose the live session, detach it from the registry, and mark the
|
|
100
|
+
* agent `parked`. No-op unless the id is adopted and live.
|
|
101
|
+
*/
|
|
102
|
+
async park(id: string): Promise<void> {
|
|
103
|
+
const adopted = this.#adopted.get(id);
|
|
104
|
+
if (!adopted) return;
|
|
105
|
+
const ref = this.#registry.get(id);
|
|
106
|
+
if (!ref?.session) return;
|
|
107
|
+
if (adopted.timer) {
|
|
108
|
+
clearTimeout(adopted.timer);
|
|
109
|
+
adopted.timer = undefined;
|
|
110
|
+
}
|
|
111
|
+
this.#parking.add(id);
|
|
112
|
+
try {
|
|
113
|
+
try {
|
|
114
|
+
await ref.session.dispose();
|
|
115
|
+
} catch (error) {
|
|
116
|
+
logger.warn("AgentLifecycleManager.park: session dispose failed", { id, error: String(error) });
|
|
117
|
+
}
|
|
118
|
+
this.#registry.detachSession(id);
|
|
119
|
+
this.#registry.setStatus(id, "parked");
|
|
120
|
+
} finally {
|
|
121
|
+
this.#parking.delete(id);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Return the live session, reviving from the sessionFile if parked.
|
|
127
|
+
* Throws a plain Error if the id is unknown or parked without a reviver.
|
|
128
|
+
* Concurrent calls share one in-flight revive.
|
|
129
|
+
*/
|
|
130
|
+
async ensureLive(id: string): Promise<AgentSession> {
|
|
131
|
+
const ref = this.#registry.get(id);
|
|
132
|
+
if (!ref) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Unknown agent "${id}" — it was never registered or has been released. If a transcript exists, read history://${id}.`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (ref.session) return ref.session;
|
|
138
|
+
const inflight = this.#revivals.get(id);
|
|
139
|
+
if (inflight) return inflight;
|
|
140
|
+
const adopted = this.#adopted.get(id);
|
|
141
|
+
if (ref.status !== "parked" || !adopted?.revive) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`Agent "${id}" is ${ref.status} and cannot be revived${adopted?.revive ? "" : " (no reviver registered)"}. Its transcript remains readable at history://${id}.`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const revival = this.#revive(id, adopted.revive, ref.sessionFile);
|
|
147
|
+
this.#revivals.set(id, revival);
|
|
148
|
+
try {
|
|
149
|
+
return await revival;
|
|
150
|
+
} finally {
|
|
151
|
+
this.#revivals.delete(id);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Hard removal: dispose if live, unregister from registry, drop timers. */
|
|
156
|
+
async release(id: string): Promise<void> {
|
|
157
|
+
const adopted = this.#adopted.get(id);
|
|
158
|
+
clearTimeout(adopted?.timer);
|
|
159
|
+
this.#adopted.delete(id);
|
|
160
|
+
const ref = this.#registry.get(id);
|
|
161
|
+
if (ref?.session) {
|
|
162
|
+
try {
|
|
163
|
+
await ref.session.dispose();
|
|
164
|
+
} catch (error) {
|
|
165
|
+
logger.warn("AgentLifecycleManager.release: session dispose failed", { id, error: String(error) });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
this.#registry.unregister(id);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Teardown everything (process exit / main session dispose). */
|
|
172
|
+
async dispose(): Promise<void> {
|
|
173
|
+
this.#unsubscribe?.();
|
|
174
|
+
this.#unsubscribe = undefined;
|
|
175
|
+
const ids = [...this.#adopted.keys()];
|
|
176
|
+
await Promise.all(ids.map(id => this.release(id)));
|
|
177
|
+
this.#revivals.clear();
|
|
178
|
+
this.#parking.clear();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async #revive(id: string, revive: AgentReviver, sessionFile: string | null): Promise<AgentSession> {
|
|
182
|
+
const session = await revive();
|
|
183
|
+
this.#registry.attachSession(id, session, sessionFile);
|
|
184
|
+
// Emits status_changed → "idle", which re-arms the TTL timer below.
|
|
185
|
+
this.#registry.setStatus(id, "idle");
|
|
186
|
+
return session;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#armTimer(id: string, adopted: AdoptedAgent): void {
|
|
190
|
+
if (adopted.idleTtlMs <= 0) return;
|
|
191
|
+
clearTimeout(adopted.timer);
|
|
192
|
+
const timer = setTimeout(() => {
|
|
193
|
+
adopted.timer = undefined;
|
|
194
|
+
void this.park(id);
|
|
195
|
+
}, adopted.idleTtlMs);
|
|
196
|
+
timer.unref?.();
|
|
197
|
+
adopted.timer = timer;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
#onRegistryEvent(event: RegistryEvent): void {
|
|
201
|
+
const adopted = this.#adopted.get(event.ref.id);
|
|
202
|
+
if (!adopted) return;
|
|
203
|
+
if (event.type === "removed") {
|
|
204
|
+
clearTimeout(adopted.timer);
|
|
205
|
+
this.#adopted.delete(event.ref.id);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (event.type !== "status_changed") return;
|
|
209
|
+
if (event.ref.status === "running") {
|
|
210
|
+
if (adopted.timer) {
|
|
211
|
+
clearTimeout(adopted.timer);
|
|
212
|
+
adopted.timer = undefined;
|
|
213
|
+
}
|
|
214
|
+
} else if (event.ref.status === "idle") {
|
|
215
|
+
this.#armTimer(event.ref.id, adopted);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AgentRegistry - Process-global registry of
|
|
2
|
+
* AgentRegistry - Process-global registry of agents (the main session plus
|
|
3
|
+
* every subagent), keyed by stable id.
|
|
3
4
|
*
|
|
4
|
-
* Tracks
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Tracks each agent's status and (when live) its AgentSession so peers can be
|
|
6
|
+
* addressed by id (`irc`, `task resume`, `history://`). Sessions are
|
|
7
|
+
* registered explicitly at creation; finished agents stay registered as
|
|
8
|
+
* `idle` (live) or `parked` (session disposed, ref + sessionFile retained for
|
|
9
|
+
* revival) and are only removed on explicit release/teardown.
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
12
|
import type { AgentSession } from "../session/agent-session";
|
|
10
13
|
|
|
11
14
|
export const MAIN_AGENT_ID = "Main";
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
/**
|
|
17
|
+
* - `running`: a turn is in flight.
|
|
18
|
+
* - `idle`: live AgentSession in memory, awaiting work. Finished agents are
|
|
19
|
+
* `idle`, not removed.
|
|
20
|
+
* - `parked`: session disposed; AgentRef + sessionFile retained, revivable.
|
|
21
|
+
* - `aborted`: hard-killed, terminal.
|
|
22
|
+
*/
|
|
23
|
+
export type AgentStatus = "running" | "idle" | "parked" | "aborted";
|
|
14
24
|
export type AgentKind = "main" | "sub";
|
|
15
25
|
|
|
16
26
|
export interface AgentRef {
|
|
@@ -19,6 +29,7 @@ export interface AgentRef {
|
|
|
19
29
|
kind: AgentKind;
|
|
20
30
|
parentId?: string;
|
|
21
31
|
status: AgentStatus;
|
|
32
|
+
/** Null exactly when parked/aborted. */
|
|
22
33
|
session: AgentSession | null;
|
|
23
34
|
sessionFile: string | null;
|
|
24
35
|
createdAt: number;
|
package/src/sdk.ts
CHANGED
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
Snowflake,
|
|
35
35
|
} from "@oh-my-pi/pi-utils";
|
|
36
36
|
import chalk from "chalk";
|
|
37
|
-
import { type AsyncJob, AsyncJobManager
|
|
37
|
+
import { type AsyncJob, AsyncJobManager } from "./async";
|
|
38
38
|
import { loadCapability } from "./capability";
|
|
39
39
|
import { type Rule, ruleCapability, setActiveRules } from "./capability/rule";
|
|
40
40
|
import { bucketRules } from "./capability/rule-buckets";
|
|
@@ -89,16 +89,18 @@ import type { HindsightSessionState } from "./hindsight/state";
|
|
|
89
89
|
import { LocalProtocolHandler, type LocalProtocolOptions } from "./internal-urls";
|
|
90
90
|
import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "./lsp/startup-events";
|
|
91
91
|
import { discoverAndLoadMCPTools, MCPManager, type MCPToolsLoadResult } from "./mcp";
|
|
92
|
-
import { resolveMemoryBackend } from "./memory-backend";
|
|
92
|
+
import { createSessionMemoryRuntimeContext, resolveMemoryBackend } from "./memory-backend";
|
|
93
93
|
import type { MnemopiSessionState } from "./mnemopi/state";
|
|
94
94
|
import asyncResultTemplate from "./prompts/tools/async-result.md" with { type: "text" };
|
|
95
95
|
import lateDiagnosticTemplate from "./prompts/tools/lsp-late-diagnostic.md" with { type: "text" };
|
|
96
|
+
import { AgentLifecycleManager } from "./registry/agent-lifecycle";
|
|
96
97
|
import { AgentRegistry, MAIN_AGENT_ID } from "./registry/agent-registry";
|
|
97
98
|
import {
|
|
98
99
|
collectEnvSecrets,
|
|
99
100
|
deobfuscateSessionContext,
|
|
100
101
|
loadSecrets,
|
|
101
102
|
obfuscateMessages,
|
|
103
|
+
obfuscateProviderContext,
|
|
102
104
|
SecretObfuscator,
|
|
103
105
|
} from "./secrets";
|
|
104
106
|
import { AgentSession } from "./session/agent-session";
|
|
@@ -134,6 +136,7 @@ import {
|
|
|
134
136
|
parseThinkingLevel,
|
|
135
137
|
resolveProvisionalAutoLevel,
|
|
136
138
|
resolveThinkingLevelForModel,
|
|
139
|
+
shouldDisableReasoning,
|
|
137
140
|
toReasoningEffort,
|
|
138
141
|
} from "./thinking";
|
|
139
142
|
import { countToolsForAutoDiscovery, resolveEffectiveToolDiscoveryMode } from "./tool-discovery/mode";
|
|
@@ -1291,7 +1294,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1291
1294
|
let hasSession = false;
|
|
1292
1295
|
let hasRegistered = false;
|
|
1293
1296
|
const enableLsp = options.enableLsp ?? true;
|
|
1294
|
-
const backgroundJobsEnabled = isBackgroundJobSupportEnabled(settings);
|
|
1295
1297
|
const asyncMaxJobs = Math.min(100, Math.max(1, settings.get("async.maxJobs") ?? 100));
|
|
1296
1298
|
const ASYNC_INLINE_RESULT_MAX_CHARS = 12_000;
|
|
1297
1299
|
const ASYNC_PREVIEW_MAX_CHARS = 4_000;
|
|
@@ -1324,7 +1326,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1324
1326
|
// (issue #1923). The `instance()` guard means later sessions also skip
|
|
1325
1327
|
// constructing an orphaned manager that nothing would ever route to.
|
|
1326
1328
|
const asyncJobManager =
|
|
1327
|
-
|
|
1329
|
+
!options.parentTaskPrefix && !AsyncJobManager.instance()
|
|
1328
1330
|
? new AsyncJobManager({
|
|
1329
1331
|
maxRunningJobs: asyncMaxJobs,
|
|
1330
1332
|
onJobComplete: async (jobId, result, job) => {
|
|
@@ -1349,6 +1351,17 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1349
1351
|
const resolvedAgentId = options.agentId ?? options.parentTaskPrefix ?? MAIN_AGENT_ID;
|
|
1350
1352
|
const resolvedAgentDisplayName =
|
|
1351
1353
|
options.agentDisplayName ?? ((options.taskDepth ?? 0) > 0 || options.parentTaskPrefix ? "sub" : "main");
|
|
1354
|
+
const agentKind = (options.taskDepth ?? 0) > 0 || options.parentTaskPrefix ? ("sub" as const) : ("main" as const);
|
|
1355
|
+
/**
|
|
1356
|
+
* Forget the agent ref on teardown — unless the agent is being parked (or is
|
|
1357
|
+
* already parked). Parking disposes the session but keeps the ref addressable
|
|
1358
|
+
* (history://, revive); only process teardown / explicit kill unregisters.
|
|
1359
|
+
*/
|
|
1360
|
+
const unregisterUnlessParked = (): void => {
|
|
1361
|
+
if (agentRegistry.get(resolvedAgentId)?.status === "parked") return;
|
|
1362
|
+
if (AgentLifecycleManager.global().isParking(resolvedAgentId)) return;
|
|
1363
|
+
agentRegistry.unregister(resolvedAgentId);
|
|
1364
|
+
};
|
|
1352
1365
|
const evalKernelOwnerId = `agent-session:${Snowflake.next()}`;
|
|
1353
1366
|
|
|
1354
1367
|
try {
|
|
@@ -1407,7 +1420,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1407
1420
|
getTurnBudget: () => sessionManager.getTurnBudget(),
|
|
1408
1421
|
recordEvalSubagentUsage: output => sessionManager.recordEvalSubagentOutput(output),
|
|
1409
1422
|
getClientBridge: () => session?.clientBridge,
|
|
1410
|
-
getCompactContext: () => session.formatCompactContext(),
|
|
1411
1423
|
queueDeferredDiagnostics: entry => session?.yieldQueue.enqueue(LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE, entry),
|
|
1412
1424
|
bumpFileMutationVersion: path => {
|
|
1413
1425
|
const next = (fileMutationVersions.get(path) ?? 0) + 1;
|
|
@@ -1791,6 +1803,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1791
1803
|
cwd,
|
|
1792
1804
|
sessionManager,
|
|
1793
1805
|
modelRegistry,
|
|
1806
|
+
() => (hasSession ? createSessionMemoryRuntimeContext(session, agentDir, cwd) : undefined),
|
|
1794
1807
|
);
|
|
1795
1808
|
|
|
1796
1809
|
credentialDisabledTarget = extensionRunner;
|
|
@@ -2080,7 +2093,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2080
2093
|
agentRegistry.register({
|
|
2081
2094
|
id: resolvedAgentId,
|
|
2082
2095
|
displayName: resolvedAgentDisplayName,
|
|
2083
|
-
kind:
|
|
2096
|
+
kind: agentKind,
|
|
2084
2097
|
parentId: options.parentTaskPrefix,
|
|
2085
2098
|
session: null,
|
|
2086
2099
|
sessionFile: sessionManager.getSessionFile() ?? null,
|
|
@@ -2138,6 +2151,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2138
2151
|
if (!obfuscator?.hasSecrets()) return converted;
|
|
2139
2152
|
return obfuscateMessages(obfuscator, converted);
|
|
2140
2153
|
};
|
|
2154
|
+
|
|
2141
2155
|
const transformContext = async (messages: AgentMessage[], _signal?: AbortSignal) => {
|
|
2142
2156
|
const withContext = await extensionRunner.emitContext(messages);
|
|
2143
2157
|
return wrapSteeringForModel(withContext);
|
|
@@ -2173,6 +2187,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2173
2187
|
systemPrompt,
|
|
2174
2188
|
model,
|
|
2175
2189
|
thinkingLevel: toReasoningEffort(effectiveThinkingLevel),
|
|
2190
|
+
disableReasoning: shouldDisableReasoning(effectiveThinkingLevel),
|
|
2176
2191
|
tools: initialTools,
|
|
2177
2192
|
},
|
|
2178
2193
|
convertToLlm: convertToLlmFinal,
|
|
@@ -2181,6 +2196,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2181
2196
|
sessionId: providerSessionId,
|
|
2182
2197
|
promptCacheKey: options.providerPromptCacheKey,
|
|
2183
2198
|
transformContext,
|
|
2199
|
+
transformProviderContext: obfuscator ? context => obfuscateProviderContext(obfuscator, context) : undefined,
|
|
2184
2200
|
steeringMode: settings.get("steeringMode") ?? "one-at-a-time",
|
|
2185
2201
|
followUpMode: settings.get("followUpMode") ?? "one-at-a-time",
|
|
2186
2202
|
interruptMode: settings.get("interruptMode") ?? "immediate",
|
|
@@ -2267,6 +2283,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2267
2283
|
thinkingLevel: autoThinking ? AUTO_THINKING : effectiveThinkingLevel,
|
|
2268
2284
|
sessionManager,
|
|
2269
2285
|
settings,
|
|
2286
|
+
autoApprove: options.autoApprove,
|
|
2270
2287
|
evalKernelOwnerId,
|
|
2271
2288
|
// Defined only for top-level sessions (creation is gated above).
|
|
2272
2289
|
// AgentSession uses this to decide whether it may dispose the global
|
|
@@ -2313,7 +2330,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2313
2330
|
ttsrManager,
|
|
2314
2331
|
obfuscator,
|
|
2315
2332
|
agentId: resolvedAgentId,
|
|
2316
|
-
agentRegistry,
|
|
2317
2333
|
providerSessionId: options.providerSessionId,
|
|
2318
2334
|
parentEvalSessionId: options.parentEvalSessionId,
|
|
2319
2335
|
});
|
|
@@ -2334,15 +2350,26 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2334
2350
|
|
|
2335
2351
|
// Attach the live session to the pre-registered ref so peers can route IRC
|
|
2336
2352
|
// messages here. Refresh sessionFile in case it was unavailable at pre-register
|
|
2337
|
-
// time. The dispose wrapper below unregisters on teardown.
|
|
2353
|
+
// time. The dispose wrapper below unregisters on teardown (unless parked).
|
|
2338
2354
|
agentRegistry.attachSession(resolvedAgentId, session, sessionManager.getSessionFile() ?? null);
|
|
2339
2355
|
{
|
|
2340
2356
|
const originalDispose = session.dispose.bind(session);
|
|
2341
2357
|
session.dispose = async () => {
|
|
2342
2358
|
try {
|
|
2359
|
+
// Reject new session work (Python/eval starts) the moment disposal
|
|
2360
|
+
// begins — the lifecycle await below opens an async gap before
|
|
2361
|
+
// AgentSession.dispose() would otherwise set its guards.
|
|
2362
|
+
session.beginDispose();
|
|
2363
|
+
if (agentKind === "main") {
|
|
2364
|
+
// Top-level teardown owns the global agent lifecycle: park timers,
|
|
2365
|
+
// adopted subagent sessions, revivers. Tear it down while shared
|
|
2366
|
+
// resources (kernels, MCP, LSP) are still live. Subagent disposal
|
|
2367
|
+
// must NOT touch the global lifecycle.
|
|
2368
|
+
await AgentLifecycleManager.global().dispose();
|
|
2369
|
+
}
|
|
2343
2370
|
await originalDispose();
|
|
2344
2371
|
} finally {
|
|
2345
|
-
|
|
2372
|
+
unregisterUnlessParked();
|
|
2346
2373
|
unsubscribeCredentialDisabled?.();
|
|
2347
2374
|
}
|
|
2348
2375
|
};
|
|
@@ -2495,7 +2522,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2495
2522
|
if (hasSession) {
|
|
2496
2523
|
await session.dispose();
|
|
2497
2524
|
} else {
|
|
2498
|
-
if (hasRegistered)
|
|
2525
|
+
if (hasRegistered) unregisterUnlessParked();
|
|
2499
2526
|
if (asyncJobManager) {
|
|
2500
2527
|
if (AsyncJobManager.instance() === asyncJobManager) {
|
|
2501
2528
|
AsyncJobManager.setInstance(undefined);
|
package/src/secrets/index.ts
CHANGED
|
@@ -4,7 +4,14 @@ import { YAML } from "bun";
|
|
|
4
4
|
import type { SecretEntry } from "./obfuscator";
|
|
5
5
|
import { compileSecretRegex } from "./regex";
|
|
6
6
|
|
|
7
|
-
export {
|
|
7
|
+
export {
|
|
8
|
+
deobfuscateSessionContext,
|
|
9
|
+
obfuscateMessages,
|
|
10
|
+
obfuscateProviderContext,
|
|
11
|
+
obfuscateProviderTools,
|
|
12
|
+
type SecretEntry,
|
|
13
|
+
SecretObfuscator,
|
|
14
|
+
} from "./obfuscator";
|
|
8
15
|
|
|
9
16
|
/**
|
|
10
17
|
* Load secrets from project-local and global secrets.yml files.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { Message,
|
|
1
|
+
import type { Context, Message, Tool } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { toolWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
|
|
2
3
|
import type { SessionContext } from "../session/session-manager";
|
|
3
4
|
import { compileSecretRegex } from "./regex";
|
|
4
5
|
|
|
@@ -184,6 +185,12 @@ export class SecretObfuscator {
|
|
|
184
185
|
return deepWalkStrings(obj, s => this.deobfuscate(s));
|
|
185
186
|
}
|
|
186
187
|
|
|
188
|
+
/** Deep-walk an object, obfuscating all string values. */
|
|
189
|
+
obfuscateObject<T>(obj: T): T {
|
|
190
|
+
if (!this.#hasAny) return obj;
|
|
191
|
+
return deepWalkStrings(obj, s => this.obfuscate(s));
|
|
192
|
+
}
|
|
193
|
+
|
|
187
194
|
/** Find the obfuscate index for a known secret value. */
|
|
188
195
|
#findObfuscateIndex(secret: string): number | undefined {
|
|
189
196
|
// Check plain mappings first
|
|
@@ -211,25 +218,34 @@ export function deobfuscateSessionContext(
|
|
|
211
218
|
// Message obfuscation (outbound to LLM)
|
|
212
219
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
213
220
|
|
|
214
|
-
/** Obfuscate all
|
|
221
|
+
/** Obfuscate all string content in LLM messages (for outbound interception). */
|
|
215
222
|
export function obfuscateMessages(obfuscator: SecretObfuscator, messages: Message[]): Message[] {
|
|
216
|
-
return
|
|
217
|
-
|
|
223
|
+
return obfuscator.obfuscateObject(messages);
|
|
224
|
+
}
|
|
218
225
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
});
|
|
226
|
+
/** Obfuscate provider request context without walking live tool schema instances. */
|
|
227
|
+
export function obfuscateProviderContext(obfuscator: SecretObfuscator | undefined, context: Context): Context {
|
|
228
|
+
if (!obfuscator?.hasSecrets()) return context;
|
|
229
|
+
return {
|
|
230
|
+
...context,
|
|
231
|
+
systemPrompt: obfuscator.obfuscateObject(context.systemPrompt),
|
|
232
|
+
messages: obfuscator.obfuscateObject(context.messages),
|
|
233
|
+
tools: obfuscateProviderTools(obfuscator, context.tools),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
230
236
|
|
|
231
|
-
|
|
232
|
-
|
|
237
|
+
/** Convert tool schemas to wire JSON Schema before obfuscating provider-visible strings. */
|
|
238
|
+
export function obfuscateProviderTools(
|
|
239
|
+
obfuscator: SecretObfuscator | undefined,
|
|
240
|
+
tools: Tool[] | undefined,
|
|
241
|
+
): Tool[] | undefined {
|
|
242
|
+
if (!tools || !obfuscator?.hasSecrets()) return tools;
|
|
243
|
+
return tools.map(tool => ({
|
|
244
|
+
...tool,
|
|
245
|
+
description: obfuscator.obfuscate(tool.description),
|
|
246
|
+
parameters: obfuscator.obfuscateObject(toolWireSchema(tool)),
|
|
247
|
+
customFormat: tool.customFormat ? obfuscator.obfuscateObject(tool.customFormat) : undefined,
|
|
248
|
+
}));
|
|
233
249
|
}
|
|
234
250
|
|
|
235
251
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -262,7 +278,7 @@ function deepWalkStrings<T>(obj: T, transform: (s: string) => string): T {
|
|
|
262
278
|
});
|
|
263
279
|
return (changed ? result : obj) as unknown as T;
|
|
264
280
|
}
|
|
265
|
-
if (obj !== null && typeof obj === "object") {
|
|
281
|
+
if (obj !== null && typeof obj === "object" && isPlainRecord(obj)) {
|
|
266
282
|
let changed = false;
|
|
267
283
|
const result: Record<string, unknown> = {};
|
|
268
284
|
for (const key of Object.keys(obj)) {
|
|
@@ -275,3 +291,8 @@ function deepWalkStrings<T>(obj: T, transform: (s: string) => string): T {
|
|
|
275
291
|
}
|
|
276
292
|
return obj;
|
|
277
293
|
}
|
|
294
|
+
|
|
295
|
+
function isPlainRecord(obj: object): obj is Record<string, unknown> {
|
|
296
|
+
const prototype = Object.getPrototypeOf(obj);
|
|
297
|
+
return prototype === Object.prototype || prototype === null;
|
|
298
|
+
}
|