pantheon-opencode 1.6.0-beta.6 → 1.6.0-beta.8
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/AGENTS.md +19 -769
- package/CHANGELOG.md +53 -0
- package/README.md +78 -9
- package/README.pt-BR.md +10 -4
- package/package.json +5 -1
- package/plugin.json +2 -2
- package/pyproject.toml +3 -1
- package/scripts/build-agents-md.mjs +159 -26
- package/scripts/install/agents-md.mjs +2 -2
- package/scripts/install/config-migration.mjs +115 -1
- package/scripts/install/interactive.mjs +1 -1
- package/scripts/install/opencode.mjs +245 -12
- package/scripts/install/shared.mjs +115 -7
- package/scripts/sync-artifacts.mjs +2 -2
- package/scripts/test-opencode-v2-sandbox.sh +155 -4
- package/scripts/validate-sandbox-install.mjs +138 -0
- package/skills-lock.json +5 -5
- package/src/agents/aphrodite.md +18 -36
- package/src/agents/demeter.md +8 -27
- package/src/agents/gaia.md +1 -0
- package/src/agents/hephaestus.md +10 -20
- package/src/agents/hermes.md +15 -37
- package/src/agents/mnemosyne.md +14 -42
- package/src/agents/prometheus.md +16 -19
- package/src/agents/talos.md +4 -1
- package/src/agents/themis.md +4 -4
- package/src/agents/zeus.md +24 -127
- package/src/instructions/agent-return-format.instructions.md +2 -3
- package/src/instructions/backend-standards.instructions.md +2 -0
- package/src/instructions/frontend-standards.instructions.md +3 -1
- package/src/instructions/memory-protocol.instructions.md +11 -121
- package/src/instructions/zeus-anti-stall.instructions.md +14 -69
- package/src/instructions/zeus-communication-rules.instructions.md +0 -1
- package/src/instructions/zeus-council-synthesis.instructions.md +32 -260
- package/src/instructions/zeus-memory-operations.instructions.md +26 -0
- package/src/instructions/zeus-timeout-retry.instructions.md +40 -87
- package/src/pantheon/v2-delegation-enforce.ts +87 -0
- package/src/pantheon/v2-execute-after.ts +186 -0
- package/src/pantheon/v2-unsupported.mjs +0 -23
- package/src/plugin-v2.ts +271 -36
- package/src/plugins/tui/dist/tui.d.ts +11 -8
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +53 -37
- package/src/plugins/tui/package-lock.json +30 -33
- package/src/plugins/tui/package.json +5 -1
- package/src/plugins/tui/src/index.tsx +103 -82
- package/src/plugins/tui/tsdown.config.ts +17 -0
- package/src/routing.yml +10 -5
- package/src/skills/README.md +3 -3
- package/src/skills/artifact-management/SKILL.md +12 -5
- package/src/skills/auto-continue/SKILL.md +37 -28
- package/src/skills/code-review-checklist/SKILL.md +5 -5
- package/src/skills/context-compression/SKILL.md +26 -879
- package/src/skills/tdd-with-agents/SKILL.md +18 -10
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: "Timeout enforcement, retry policies, subtask dispatch, and timeout tracking for Zeus"
|
|
3
3
|
name: "Zeus Timeout & Retry"
|
|
4
|
-
|
|
4
|
+
agents: [zeus]
|
|
5
|
+
fallback: shared
|
|
5
6
|
---
|
|
6
7
|
|
|
7
8
|
# ⏱️ TIMEOUT & RETRY ENFORCEMENT
|
|
@@ -10,123 +11,75 @@ When a delegated agent does not respond in time, enforce the timeout policy from
|
|
|
10
11
|
|
|
11
12
|
## Timeout Behavior by Agent Role
|
|
12
13
|
|
|
13
|
-
| Agent Role | Timeout | Retry Policy |
|
|
14
|
-
|
|
15
|
-
| Explorer (@apollo) | 60s |
|
|
16
|
-
| Implementer (@hermes, @aphrodite, @demeter) | 180s |
|
|
17
|
-
| Reviewer (@themis) | 120s |
|
|
18
|
-
| Infrastructure (@prometheus) | 300s |
|
|
19
|
-
| Hotfix (@talos) | 30s |
|
|
20
|
-
| Remote Sensing (@gaia) | 120s |
|
|
14
|
+
| Agent Role | Timeout | Retry Policy | Partial Results OK? | Reasoning Effort |
|
|
15
|
+
|------------|---------|--------------|---------------------|------------------|
|
|
16
|
+
| Explorer (@apollo) | 60s | 1 retry | ✅ Yes | low |
|
|
17
|
+
| Implementer (@hermes, @aphrodite, @demeter) | 180s | 1 retry | ❌ No | medium |
|
|
18
|
+
| Reviewer (@themis) | 120s | 1 retry | ❌ No | high |
|
|
19
|
+
| Infrastructure (@prometheus) | 300s | 1 retry | ❌ No | medium |
|
|
20
|
+
| Hotfix (@talos) | 30s | 1 retry | ✅ Yes | low |
|
|
21
|
+
| Remote Sensing (@gaia) | 120s | 1 retry | ✅ Yes | high |
|
|
21
22
|
|
|
22
23
|
## Retry Flow
|
|
23
24
|
|
|
24
25
|
```
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
│
|
|
29
|
-
├─ retries exhausted → FALLBACK CHAIN:
|
|
30
|
-
│ ├─ fallback[0] exists? → dispatch to fallback agent
|
|
31
|
-
│ │ └─ keep original task spec + context; add error context
|
|
32
|
-
│ ├─ fallback[1] exists? → dispatch to second fallback
|
|
33
|
-
│ └─ all fallbacks failed? → ESCALATE
|
|
34
|
-
│
|
|
35
|
-
├─ no fallbacks defined → return TIMEOUT error to user
|
|
36
|
-
│
|
|
37
|
-
└─ ESCALATE: "Agent X failed after N retries. Fallbacks Y, Z also failed.
|
|
38
|
-
Options: (a) try different agent, (b) simplify scope, (c) manual"
|
|
26
|
+
Initial attempt → timeout/failure → log it → make at most one retry
|
|
27
|
+
└─ retry fails → try fallback agents left-to-right with original task/context
|
|
28
|
+
└─ all fallbacks fail (or none exist) → report the chain and escalate
|
|
39
29
|
```
|
|
30
|
+
`background_delegation.retry_count: 1` means one retry after the initial attempt, not one total attempt. Never restart an exhausted chain automatically.
|
|
40
31
|
|
|
41
|
-
|
|
32
|
+
Apply the retry only to a transient dispatch failure (timeout, crash, or empty response). A failed test, validation, or migration is a result to diagnose, not a reason to repeat the same command or agent blindly. Track agents already tried: each agent may be retried at most once and each listed fallback may be tried at most once. For auth/security/data/schema work, a fallback must preserve specialist competence, Themis review, and human approval; otherwise stop and escalate. Never let full-auto bypass these gates.
|
|
33
|
+
An agent refusal or scope-boundary response is not a transient failure: do not retry it with a rephrased prompt. Correct the route once if the right specialist is clear; otherwise stop and ask.
|
|
42
34
|
|
|
43
35
|
## Fallback Chain Definitions
|
|
44
36
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
| @apollo | @athena (plan scoped task) | @hermes (implement search) | @zeus |
|
|
50
|
-
| @hermes | @talos (minimal fix) | @athena (replan + simplify) | @zeus |
|
|
51
|
-
| @aphrodite | @talos (CSS/UX fix) | @hermes (generic fallback) | @zeus |
|
|
52
|
-
| @demeter | @hermes (generic backend) | @athena (replan schema) | @zeus |
|
|
53
|
-
| @themis | @zeus (direct escalation) | — | user |
|
|
54
|
-
| @prometheus | @hermes (config/deploy) | @zeus | user |
|
|
55
|
-
| @talos | @hermes (full implementation) | — | @zeus |
|
|
56
|
-
| @hephaestus | @nyx (observability debug) | @hermes (generic) | @zeus |
|
|
57
|
-
| @nyx | @hermes (generic) | — | @zeus |
|
|
58
|
-
| @iris | @zeus (manual override) | — | user |
|
|
59
|
-
| @mnemosyne | @zeus (manual) | — | user |
|
|
37
|
+
`routing.yml` → `fallback_chains` is the canonical agent-by-agent mapping. Evaluate
|
|
38
|
+
the configured chain left-to-right; if an agent has no configured chain, stop
|
|
39
|
+
and escalate rather than guessing a fallback. Each listed fallback may be tried
|
|
40
|
+
at most once, and an exhausted chain is never restarted automatically.
|
|
60
41
|
|
|
61
|
-
###
|
|
62
|
-
When ALL fallbacks fail:
|
|
63
|
-
1. Log the full failure chain: which agents were tried, what error each returned
|
|
64
|
-
2. Report to user: "Task [X] failed. Tried: [agent list]. Error: [summary]."
|
|
65
|
-
3. Offer options: (a) try different approach, (b) simplify scope, (c) manual fix
|
|
66
|
-
4. NEVER retry the same chain automatically — break the cycle
|
|
42
|
+
### Refusal Is a Routing Error
|
|
67
43
|
|
|
68
|
-
|
|
69
|
-
|
|
44
|
+
For a domain/scope refusal, do not retry with a rephrased prompt. Correct the
|
|
45
|
+
route once only when the right specialist is clear; otherwise stop and ask.
|
|
46
|
+
Record one `DelegationCacheDecision` telemetry item for the corrected route,
|
|
47
|
+
including `reroute_from`, `reroute_to`, and `delegation_id` when available.
|
|
70
48
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
Files already explored: backend/routers/auth.py, backend/services/auth_service.py.
|
|
74
|
-
New task: add refresh token rotation.
|
|
75
|
-
```
|
|
49
|
+
### Escalation Protocol
|
|
50
|
+
When all fallbacks fail, report the tried agents and errors, offer a different approach/simpler scope/manual fix, and stop. Never retry the same chain automatically.
|
|
76
51
|
|
|
77
|
-
|
|
52
|
+
### Session Reuse Check
|
|
53
|
+
Check for a reusable session before dispatch and obey `session_max` in `routing.yml`.
|
|
78
54
|
|
|
79
55
|
---
|
|
80
56
|
|
|
81
57
|
# 📦 SUBTASK DISPATCH (Lightweight Delegation)
|
|
82
58
|
|
|
83
|
-
Subtask is a bounded, low-risk delegation mode that **skips** the standard artifact lifecycle.
|
|
59
|
+
Subtask is a bounded, low-risk delegation mode that **skips** the standard artifact lifecycle. Choose it by risk and need for review, not by a fixed file/line count.
|
|
84
60
|
|
|
85
61
|
## When to Use Subtask vs Full Task
|
|
86
62
|
|
|
87
|
-
|
|
63
|
+
Use the lightest path that safely meets the request. A multi-file but bounded low-risk change does not automatically need a plan/artifact phase.
|
|
88
64
|
|
|
89
65
|
### Subtask Decision Tree (run BEFORE every delegation)
|
|
90
66
|
|
|
91
67
|
```
|
|
92
|
-
□
|
|
93
|
-
□
|
|
94
|
-
□
|
|
95
|
-
□ Data: No data loss risk, no migration? [YES→continue | NO→full task]
|
|
96
|
-
□ Review: Output does NOT feed into Themis review? [YES→continue | NO→full task]
|
|
97
|
-
|
|
98
|
-
ALL YES → subtask (skip artifact + Themis)
|
|
99
|
-
ANY NO → full task (IMPL artifact + Themis review mandatory)
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### Comparison Table
|
|
68
|
+
□ Is the scope clear, bounded, and reversible? [YES→continue | NO→clarify/plan]
|
|
69
|
+
□ Does it avoid auth/security, data/schema, and destructive risk? [YES→continue | NO→full review/gates]
|
|
70
|
+
□ Is there no required Themis/audit handoff for this change? [YES→continue | NO→preserve that review]
|
|
103
71
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
| Risk | Low (no security/data implications) | Any risk level |
|
|
108
|
-
| Artifact | ❌ No IMPL artifact | ✅ IMPL artifact required |
|
|
109
|
-
| Themis review | ❌ None | ✅ Mandatory |
|
|
110
|
-
| Use case | Apollo discovery, Talos hotfix, bounded fix | Feature implementation, migration, API change |
|
|
111
|
-
|
|
112
|
-
### Concrete Examples
|
|
113
|
-
|
|
114
|
-
| Task | Scope | Risk | Subtask? | Why |
|
|
115
|
-
|------|-------|------|----------|-----|
|
|
116
|
-
| Fix typo in CSS class | 1 file, 1 line | None | ✅ | Bounded, no security impact |
|
|
117
|
-
| Add error handling to existing endpoint | 1 file, 5 lines | Low | ✅ | No schema change |
|
|
118
|
-
| Implement login endpoint | 2+ files, 50+ lines | High (auth) | ❌ | Security-critical, needs Themis |
|
|
119
|
-
| Database migration | 1 file, 15 lines | High (data) | ❌ | Data loss risk, needs rollback |
|
|
120
|
-
| Apollo codebase search | 0 files | None | ✅ | Read-only investigation |
|
|
121
|
-
| Update README | 1 file, 3 lines | None | ✅ | Documentation only |
|
|
72
|
+
ALL YES → direct/lightweight execution; no routine plan or artifact
|
|
73
|
+
ANY NO → add only the planning, artifact, specialist review, and approval gates the risk requires
|
|
74
|
+
```
|
|
122
75
|
|
|
123
76
|
### Safety Rules
|
|
124
|
-
1. **Bounded scope** —
|
|
77
|
+
1. **Bounded scope** — can be a small multi-file change or a read-only investigation
|
|
125
78
|
2. **Low risk** — no security implications, no data loss, no breaking changes
|
|
126
|
-
3. **No Themis dependency** — output
|
|
79
|
+
3. **No required Themis dependency** — sensitive/material output retains its review gate
|
|
127
80
|
|
|
128
81
|
## Subtask Return Format
|
|
129
|
-
Return the
|
|
82
|
+
Return the required `## subtask_summary` fields defined in `## Agent Return Format`; include `memory_context` when memory was used.
|
|
130
83
|
|
|
131
84
|
## Timeout Parcial (Partial Results)
|
|
132
85
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** Caller/target policy for V2's permission.evaluate hook. */
|
|
2
|
+
|
|
3
|
+
export const PANTHEON_AGENTS: ReadonlySet<string> = new Set([
|
|
4
|
+
'zeus',
|
|
5
|
+
'athena',
|
|
6
|
+
'apollo',
|
|
7
|
+
'hermes',
|
|
8
|
+
'aphrodite',
|
|
9
|
+
'demeter',
|
|
10
|
+
'themis',
|
|
11
|
+
'prometheus',
|
|
12
|
+
'hephaestus',
|
|
13
|
+
'nyx',
|
|
14
|
+
'gaia',
|
|
15
|
+
'iris',
|
|
16
|
+
'mnemosyne',
|
|
17
|
+
'talos',
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
export interface V2PermissionEvent {
|
|
21
|
+
sessionID?: unknown
|
|
22
|
+
resources?: unknown
|
|
23
|
+
/** Optional event identity is checked but never used as authority. */
|
|
24
|
+
agent?: unknown
|
|
25
|
+
/** Mutable decision fields exposed by OpenCode V2's evaluate hook. */
|
|
26
|
+
effect?: unknown
|
|
27
|
+
message?: unknown
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AuthoritativeSession {
|
|
31
|
+
id?: unknown
|
|
32
|
+
agent?: unknown
|
|
33
|
+
parentID?: unknown
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type V2DelegationDecision = 'allow' | 'deny' | 'passthrough' | 'preserve-deny'
|
|
37
|
+
|
|
38
|
+
function agentName(value: unknown): string | undefined {
|
|
39
|
+
if (typeof value !== 'string') return undefined
|
|
40
|
+
const valueNormalized = value.trim().toLowerCase()
|
|
41
|
+
return valueNormalized.length === 0 ? undefined : valueNormalized
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Extract the target from the host's resource string or `{ target }` entry. */
|
|
45
|
+
export function v2ResourceTarget(resource: unknown): string | undefined {
|
|
46
|
+
if (typeof resource === 'string') return agentName(resource)
|
|
47
|
+
if (resource === null || typeof resource !== 'object' || Array.isArray(resource)) return undefined
|
|
48
|
+
return agentName((resource as { target?: unknown }).target)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Decide whether a permission evaluation is a Pantheon-managed delegation.
|
|
53
|
+
* Native/custom-to-native/custom calls are left entirely to host permissions.
|
|
54
|
+
*/
|
|
55
|
+
export function evaluateV2Delegation(
|
|
56
|
+
event: V2PermissionEvent,
|
|
57
|
+
session: AuthoritativeSession | undefined,
|
|
58
|
+
existingStatus?: unknown,
|
|
59
|
+
): V2DelegationDecision {
|
|
60
|
+
if (existingStatus === 'deny') return 'preserve-deny'
|
|
61
|
+
|
|
62
|
+
if (!Array.isArray(event.resources) || event.resources.length === 0) return 'deny'
|
|
63
|
+
const rawTargets = event.resources.map(v2ResourceTarget)
|
|
64
|
+
if (rawTargets.some((target) => target === undefined)) return 'deny'
|
|
65
|
+
const targets = rawTargets.filter((target): target is string => target !== undefined)
|
|
66
|
+
const pantheonTargets = targets.filter((target) => PANTHEON_AGENTS.has(target))
|
|
67
|
+
if (pantheonTargets.length === 0) return 'passthrough'
|
|
68
|
+
const sessionID = typeof event.sessionID === 'string' ? event.sessionID : undefined
|
|
69
|
+
const caller = agentName(session?.agent)
|
|
70
|
+
if (
|
|
71
|
+
sessionID === undefined ||
|
|
72
|
+
session === undefined ||
|
|
73
|
+
session.id !== sessionID ||
|
|
74
|
+
caller === undefined
|
|
75
|
+
) {
|
|
76
|
+
return 'deny'
|
|
77
|
+
}
|
|
78
|
+
const eventCaller = agentName(event.agent)
|
|
79
|
+
if (eventCaller !== undefined && eventCaller !== caller) return 'deny'
|
|
80
|
+
if (session.parentID !== undefined && session.parentID !== null && session.parentID !== '') {
|
|
81
|
+
return 'deny'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const callerMayTarget = (target: string): boolean =>
|
|
85
|
+
caller === 'zeus' || ((caller === 'athena' || caller === 'hermes') && target === 'apollo')
|
|
86
|
+
return pantheonTargets.every(callerMayTarget) ? 'allow' : 'deny'
|
|
87
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V2 `execute.after` parity adapter — reproduces the V1 post-tool chain on the
|
|
3
|
+
* V2 tool-event surface.
|
|
4
|
+
*
|
|
5
|
+
* V1 (`src/plugin.ts`) wires a single `tool.execute.after` handler that runs,
|
|
6
|
+
* in order:
|
|
7
|
+
* 1. task-result-guard — an empty native `task()` result becomes an explicit
|
|
8
|
+
* error so the parent LLM cannot read silence as success;
|
|
9
|
+
* 2. context-sandbox — oversized read/grep/glob/webfetch output is truncated
|
|
10
|
+
* with a marker + metadata;
|
|
11
|
+
* 3. read-enhancer — a `read` result gains per-line hashline tags
|
|
12
|
+
* (`N#TAG|content`) so `hashline_edit` refs resolve.
|
|
13
|
+
*
|
|
14
|
+
* On a V2-only install (`opencode.json` `plugins` declares only `plugin-v2`),
|
|
15
|
+
* `src/plugin.ts` is NOT loaded, so that chain does not exist. `plugin-v2.ts`
|
|
16
|
+
* registered an `execute.after` hook but with an empty body, so the chain was
|
|
17
|
+
* dead on V2. This module rebuilds it against the REAL V2 event shape.
|
|
18
|
+
*
|
|
19
|
+
* The V2 event (opencode 2.0.x `ToolHooks["execute.after"]`) is ONE object:
|
|
20
|
+
*
|
|
21
|
+
* { tool, sessionID, agent, messageID, id, input } & (
|
|
22
|
+
* { status: 'completed'; result: Tool.Result } |
|
|
23
|
+
* { status: 'error'; error: Tool.Error } )
|
|
24
|
+
*
|
|
25
|
+
* where `Tool.Result = { output?, content?, metadata? }`. This adapter only
|
|
26
|
+
* acts on `status === 'completed'`; an ERROR event is left untouched — V1's own
|
|
27
|
+
* behaviour on a tool error was never verified, so this module does not claim
|
|
28
|
+
* after-on-error semantics.
|
|
29
|
+
*
|
|
30
|
+
* Fail-safe: every path is wrapped so an unexpected shape can never break the
|
|
31
|
+
* tool result the host is about to surface.
|
|
32
|
+
*
|
|
33
|
+
* @module pantheon/v2-execute-after
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import {
|
|
37
|
+
type ContextSandboxConfig,
|
|
38
|
+
createContextSandbox,
|
|
39
|
+
resolveSandboxConfig,
|
|
40
|
+
DEFAULT_CONFIG as SANDBOX_DEFAULT_CONFIG,
|
|
41
|
+
} from './context-sandbox.ts'
|
|
42
|
+
import { createReadEnhancer } from './hashline/read-enhancer.ts'
|
|
43
|
+
import { createTaskResultGuard } from './task-result-guard.ts'
|
|
44
|
+
|
|
45
|
+
/** The `Tool.Result` fields this adapter reads and writes back. */
|
|
46
|
+
export interface V2ToolResult {
|
|
47
|
+
output?: unknown
|
|
48
|
+
content?: unknown
|
|
49
|
+
metadata?: unknown
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The completed, non-error `execute.after` event as the 2.0.x host dispatches it. */
|
|
53
|
+
export interface V2ExecuteAfterEvent {
|
|
54
|
+
tool?: unknown
|
|
55
|
+
sessionID?: unknown
|
|
56
|
+
id?: unknown
|
|
57
|
+
input?: unknown
|
|
58
|
+
status?: unknown
|
|
59
|
+
result?: unknown
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Options for {@link createV2ExecuteAfter}. */
|
|
63
|
+
export interface V2ExecuteAfterOptions {
|
|
64
|
+
/**
|
|
65
|
+
* Sandbox config, normally derived from `ctx.options.context_sandbox` via
|
|
66
|
+
* {@link resolveSandboxConfig}. Defaults to the shared defaults.
|
|
67
|
+
*/
|
|
68
|
+
sandbox?: ContextSandboxConfig
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Which `Tool.Result` field carries the (mutable) text this adapter enhances. */
|
|
72
|
+
type TextField = 'output' | 'content' | 'content-array'
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Extract the mutable text from a `Tool.Result`.
|
|
76
|
+
*
|
|
77
|
+
* Preference order mirrors what a session renders: a declared string `output`
|
|
78
|
+
* first, then a string `content`, then the concatenation of `content` text
|
|
79
|
+
* parts. A `content` array containing non-text (file) parts is deliberately NOT
|
|
80
|
+
* enhanced — replacing it would corrupt the structured payload, so it is
|
|
81
|
+
* reported as unextractable and the whole event becomes a no-op.
|
|
82
|
+
*
|
|
83
|
+
* @returns `{ text, field }` or `null` when there is nothing to enhance.
|
|
84
|
+
*/
|
|
85
|
+
function extractText(result: V2ToolResult): { text: string; field: TextField } | null {
|
|
86
|
+
if (typeof result.output === 'string') return { text: result.output, field: 'output' }
|
|
87
|
+
if (typeof result.content === 'string') return { text: result.content, field: 'content' }
|
|
88
|
+
if (Array.isArray(result.content)) {
|
|
89
|
+
const parts = result.content
|
|
90
|
+
const allText = parts.every(
|
|
91
|
+
(part) => typeof part === 'string' || (part as { type?: unknown })?.type === 'text',
|
|
92
|
+
)
|
|
93
|
+
if (!allText || parts.length === 0) return null
|
|
94
|
+
const text = parts
|
|
95
|
+
.map((part) => (typeof part === 'string' ? part : ((part as { text?: string }).text ?? '')))
|
|
96
|
+
.join('\n')
|
|
97
|
+
return { text, field: 'content-array' }
|
|
98
|
+
}
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Write enhanced text back into the SAME field it came from, preserving the
|
|
104
|
+
* result's other fields.
|
|
105
|
+
*/
|
|
106
|
+
function writeText(result: V2ToolResult, field: TextField, text: string): void {
|
|
107
|
+
if (field === 'output') {
|
|
108
|
+
result.output = text
|
|
109
|
+
} else if (field === 'content') {
|
|
110
|
+
result.content = text
|
|
111
|
+
} else {
|
|
112
|
+
result.content = [{ type: 'text', text }]
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Build the V2 `execute.after` handler that runs the V1 parity chain.
|
|
118
|
+
*
|
|
119
|
+
* @param options - see {@link V2ExecuteAfterOptions}
|
|
120
|
+
* @returns a handler suitable for `ctx.tool.hook('execute.after', …)`
|
|
121
|
+
*/
|
|
122
|
+
export function createV2ExecuteAfter(
|
|
123
|
+
options: V2ExecuteAfterOptions = {},
|
|
124
|
+
): (event: unknown) => Promise<void> {
|
|
125
|
+
const taskResultGuard = createTaskResultGuard()
|
|
126
|
+
const sandbox = createContextSandbox(options.sandbox ?? SANDBOX_DEFAULT_CONFIG)
|
|
127
|
+
const readEnhancer = createReadEnhancer()
|
|
128
|
+
|
|
129
|
+
return async function v2ExecuteAfter(event: unknown): Promise<void> {
|
|
130
|
+
try {
|
|
131
|
+
if (event === null || typeof event !== 'object' || Array.isArray(event)) return
|
|
132
|
+
const payload = event as V2ExecuteAfterEvent
|
|
133
|
+
// Only a completed result has a result to augment; an error event is left
|
|
134
|
+
// untouched (no after-on-error semantics are claimed).
|
|
135
|
+
if (payload.status !== 'completed') return
|
|
136
|
+
|
|
137
|
+
const result = payload.result
|
|
138
|
+
if (result === null || typeof result !== 'object' || Array.isArray(result)) return
|
|
139
|
+
|
|
140
|
+
const tool = typeof payload.tool === 'string' ? payload.tool : ''
|
|
141
|
+
const sessionID = typeof payload.sessionID === 'string' ? payload.sessionID : ''
|
|
142
|
+
const callID = typeof payload.id === 'string' ? payload.id : ''
|
|
143
|
+
|
|
144
|
+
const extracted = extractText(result as V2ToolResult)
|
|
145
|
+
// A `task` result may carry no text at all — that IS the empty-result
|
|
146
|
+
// failure the guard exists to surface, so synthesize an empty string and
|
|
147
|
+
// let the guard replace it. Other tools with no text are a no-op.
|
|
148
|
+
if (extracted === null && tool !== 'task') return
|
|
149
|
+
const original = extracted === null ? { text: '', field: 'output' as TextField } : extracted
|
|
150
|
+
|
|
151
|
+
const input = { tool, sessionID, callID, args: payload.input }
|
|
152
|
+
const output: {
|
|
153
|
+
output: string
|
|
154
|
+
metadata?: Record<string, unknown>
|
|
155
|
+
} = { output: original.text }
|
|
156
|
+
|
|
157
|
+
await taskResultGuard(input, output)
|
|
158
|
+
await sandbox(input, output)
|
|
159
|
+
await readEnhancer(input, output)
|
|
160
|
+
|
|
161
|
+
if (output.output !== original.text) {
|
|
162
|
+
writeText(result as V2ToolResult, original.field, output.output)
|
|
163
|
+
}
|
|
164
|
+
// Surface sandbox metadata onto the result (merge, never replace). It is
|
|
165
|
+
// only present when the sandbox actually truncated something.
|
|
166
|
+
if (output.metadata && Object.keys(output.metadata).length > 0) {
|
|
167
|
+
;(result as V2ToolResult).metadata = {
|
|
168
|
+
...((result as V2ToolResult).metadata as Record<string, unknown> | undefined),
|
|
169
|
+
...output.metadata,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} catch {
|
|
173
|
+
// Fail-safe: an unexpected shape must never break the tool result.
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Resolve a sandbox config from a V2 plugin's `ctx.options.context_sandbox`.
|
|
180
|
+
* Exposed so `plugin-v2.ts` resolves it the SAME way V1's config hook does,
|
|
181
|
+
* rather than silently constructing defaults.
|
|
182
|
+
*/
|
|
183
|
+
export function resolveV2SandboxConfig(options: unknown): ContextSandboxConfig {
|
|
184
|
+
const raw = (options as { context_sandbox?: unknown } | null | undefined)?.context_sandbox
|
|
185
|
+
return resolveSandboxConfig(raw)
|
|
186
|
+
}
|
|
@@ -74,29 +74,6 @@ export const V2_UNSUPPORTED_FEATURE_SEED = Object.freeze([
|
|
|
74
74
|
// pantheon_goal_create/get/update are therefore absent from the V2 surface
|
|
75
75
|
// rather than registered as non-functional placeholders.
|
|
76
76
|
'goal-tools',
|
|
77
|
-
// Adapter limitation, not a host gap — TWO separate reasons, which are easy
|
|
78
|
-
// to conflate and were once stated wrongly (see the v2EnforcementGuard
|
|
79
|
-
// comment in src/plugin-v2.ts for the long form):
|
|
80
|
-
// - MISSING WIRING: V2 keeps no session→agent map (the host puts the agent
|
|
81
|
-
// on the execute.before event), so there is no `getSessionAgent` to pass.
|
|
82
|
-
// The guard would then call isDelegationAllowed(undefined, target) → false
|
|
83
|
-
// and throw "caller agent is unavailable" on EVERY task() call.
|
|
84
|
-
// - UNWIRED HIERARCHY: V2 seeds no session→parent map anywhere, and an
|
|
85
|
-
// unseeded SessionHierarchyRegistry gates exactly two things — the depth-2
|
|
86
|
-
// child deny and the root-session gate. Unseeded, isChild is always false,
|
|
87
|
-
// so neither can be trusted. UNWIRED, not impossible: the V2
|
|
88
|
-
// `session.created` event carries `properties.info: Session` with
|
|
89
|
-
// `parentID?`, the same field V1 seeds from on the same event, and one
|
|
90
|
-
// line in onSessionCreated would seed live sessions. V1's OTHER source —
|
|
91
|
-
// the client.session.list() startup seed for sessions predating plugin
|
|
92
|
-
// load — has no V2 equivalent at all, because V2's PluginContext exposes
|
|
93
|
-
// no `client`. Whether a live 2.0.22 host populates info.parentID on that
|
|
94
|
-
// event is UNVERIFIED: the canary records Object.keys(ctx), not payloads.
|
|
95
|
-
// Note the hierarchy is NOT what would deny: isRoot reports `true` for an
|
|
96
|
-
// unknown session while unseeded, which PASSES the root gate.
|
|
97
|
-
// Read-only depth-2 still holds via the blocked-tool list in the guard, so
|
|
98
|
-
// this marker records an unenforced matrix, not an unenforced depth limit.
|
|
99
|
-
'delegation-matrix',
|
|
100
77
|
])
|
|
101
78
|
|
|
102
79
|
/**
|