pantheon-opencode 1.6.0-beta.1 → 1.6.0-beta.10
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.global.md +182 -0
- package/AGENTS.md +10 -891
- package/CHANGELOG.md +326 -0
- package/README.md +496 -32
- package/README.pt-BR.md +285 -20
- package/bin/pantheon-init.mjs +26 -3
- package/commands/pantheon-audit.md +7 -1
- package/commands/pantheon-consolidate.md +1 -1
- package/commands/pantheon-deepwork.md +14 -3
- package/commands/pantheon-focus.md +17 -0
- package/commands/pantheon-vision.md +61 -12
- package/package.json +21 -14
- package/plugin.json +2 -2
- package/pyproject.toml +8 -6
- package/scripts/build-agents-md.mjs +374 -56
- package/scripts/changelog-merge.mjs +161 -0
- package/scripts/{code_mode_server.py → code_mode.py} +3 -3
- package/scripts/cost.mjs +244 -41
- package/scripts/doctor.mjs +151 -1
- package/scripts/install/agents-md.mjs +4 -4
- package/scripts/install/config-migration.mjs +317 -4
- package/scripts/install/health-check.mjs +21 -19
- package/scripts/install/interactive.mjs +1 -1
- package/scripts/install/opencode-version.mjs +246 -4
- package/scripts/install/opencode.mjs +627 -179
- package/scripts/install/plugin.mjs +43 -9
- package/scripts/install/shared.mjs +120 -12
- package/scripts/install/vision-prompt.mjs +161 -0
- package/scripts/install-mcp.mjs +28 -9
- package/scripts/{mcp_persistence_server.py → mcp_persistence.py} +2 -2
- package/{src/mcp/mcp_resources_server.py → scripts/mcp_resources.py} +2 -2
- package/scripts/{memory_mcp_server.py → memory_mcp.py} +2 -2
- package/scripts/probe-context-rehydrate.mjs +8 -2
- package/scripts/probe-pantheon-cost.mjs +71 -15
- package/scripts/run-node-tests.mjs +189 -0
- package/scripts/sync-artifacts.mjs +26 -13
- package/scripts/sync-tui.mjs +24 -3
- package/scripts/test-opencode-v2-sandbox.sh +180 -26
- package/scripts/validate-routing.mjs +3 -3
- package/scripts/validate-sandbox-install.mjs +138 -0
- package/scripts/versioning.mjs +13 -1
- package/scripts/vision-status.mjs +682 -0
- package/skills-lock.json +8 -8
- package/src/agents/aphrodite.md +20 -37
- package/src/agents/apollo.md +7 -3
- package/src/agents/athena.md +63 -35
- package/src/agents/demeter.md +8 -28
- package/src/agents/gaia.md +1 -1
- package/src/agents/hephaestus.md +10 -21
- package/src/agents/hermes.md +15 -38
- package/src/agents/iris.md +1 -1
- package/src/agents/mnemosyne.md +18 -54
- package/src/agents/nyx.md +1 -1
- package/src/agents/prometheus.md +16 -20
- package/src/agents/talos.md +10 -2
- package/src/agents/themis.md +5 -5
- package/src/agents/zeus.md +99 -129
- package/src/instructions/agent-return-format.instructions.md +18 -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 -110
- package/src/instructions/zeus-anti-stall.instructions.md +14 -61
- 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/mcp/{code_mode_server.py → code_mode.py} +3 -3
- package/src/mcp/eval_store.py +2 -2
- package/src/mcp/{mcp_persistence_server.py → mcp_persistence.py} +2 -2
- package/{scripts/mcp_resources_server.py → src/mcp/mcp_resources.py} +2 -2
- package/src/mcp/{memory_mcp_server.py → memory_mcp.py} +2 -2
- package/src/mcp/{pantheon_vision_server.py → pantheon_vision.py} +286 -42
- package/src/mcp/requirements-mcp.txt +6 -4
- package/src/mcp/requirements-vision.txt +2 -2
- package/src/pantheon/cost-command.ts +399 -101
- package/src/pantheon/delegation-enforce.ts +10 -1
- package/src/pantheon/presets.d.mts +0 -8
- package/src/pantheon/presets.mjs +5 -32
- package/src/pantheon/v2-bounded-read.ts +640 -0
- package/src/pantheon/v2-delegation-enforce.ts +214 -0
- package/src/pantheon/v2-execute-after.ts +186 -0
- package/src/pantheon/v2-tools.ts +429 -0
- package/src/pantheon/v2-unsupported.d.mts +11 -0
- package/src/pantheon/v2-unsupported.mjs +90 -0
- package/src/pantheon/v2-vision.ts +622 -0
- package/src/pantheon/vision-local.ts +531 -0
- package/src/pantheon/vision.ts +1160 -115
- package/src/plugin-v2.ts +1145 -219
- package/src/plugin.ts +12 -9
- package/src/plugins/tui/dist/server.d.ts +1 -2
- package/src/plugins/tui/dist/server.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui-v2.d.ts +154 -0
- package/src/plugins/tui/dist/tui-v2.d.ts.map +1 -0
- package/src/plugins/tui/dist/tui-v2.js +659 -0
- package/src/plugins/tui/dist/tui-v2.js.map +1 -0
- package/src/plugins/tui/dist/tui.d.ts +85 -82
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +72 -59
- package/src/plugins/tui/index-v2.ts +6 -0
- package/src/plugins/tui/package-lock.json +5516 -1353
- package/src/plugins/tui/package.json +22 -12
- package/src/plugins/tui/src/index-v2.tsx +734 -0
- package/src/plugins/tui/src/index.tsx +106 -85
- package/src/plugins/tui/src/opencode-v2.d.ts +13 -0
- package/src/plugins/tui/tsconfig.build.json +5 -0
- package/src/plugins/tui/tsconfig.typecheck.json +5 -0
- package/src/plugins/tui/tsdown.config.ts +22 -2
- package/src/plugins/tui/tui-v2.ts +2 -0
- package/src/plugins/tui/tui.ts +3 -0
- package/src/routing.yml +11 -21
- package/src/skills/README.md +3 -3
- package/src/skills/agent-coordination/SKILL.md +7 -2
- package/src/skills/artifact-management/SKILL.md +12 -5
- package/src/skills/auto-continue/SKILL.md +39 -30
- package/src/skills/code-review-checklist/SKILL.md +5 -5
- package/src/skills/context-compression/SKILL.md +26 -879
- package/src/skills/session-goal/SKILL.md +14 -5
- package/src/skills/spec-driven-development/SKILL.md +3 -1
- package/src/skills/tdd-with-agents/SKILL.md +18 -10
- package/opencode.json +0 -58
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/** V2 caller/target policy and one-shot host tool-origin correlation. */
|
|
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
|
+
action?: unknown
|
|
23
|
+
resources?: unknown
|
|
24
|
+
/** Optional event identity is cross-checked against the host tool event. */
|
|
25
|
+
agent?: unknown
|
|
26
|
+
source?: unknown
|
|
27
|
+
/** Mutable decision fields exposed by OpenCode V2's evaluate hook. */
|
|
28
|
+
effect?: unknown
|
|
29
|
+
message?: unknown
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface V2SubagentOrigin {
|
|
33
|
+
sessionID: string
|
|
34
|
+
messageID: string
|
|
35
|
+
callID: string
|
|
36
|
+
caller: string
|
|
37
|
+
target: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface StoredV2SubagentOrigin extends V2SubagentOrigin {
|
|
41
|
+
capturedAt: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const ORIGIN_TTL_MS = 60_000
|
|
45
|
+
const MAX_PENDING_ORIGINS = 256
|
|
46
|
+
|
|
47
|
+
function originKey(sessionID: string, messageID: string, callID: string): string {
|
|
48
|
+
return JSON.stringify([sessionID, messageID, callID])
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function nonEmptyString(value: unknown): string | undefined {
|
|
52
|
+
if (typeof value !== 'string') return undefined
|
|
53
|
+
const trimmed = value.trim()
|
|
54
|
+
return trimmed.length === 0 ? undefined : trimmed
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* One-shot correlation between the host's tool.execute.before event and the
|
|
59
|
+
* permission evaluation emitted from inside that same native tool call.
|
|
60
|
+
* Entries are short-lived and bounded; missing, stale, or uncorrelated origin
|
|
61
|
+
* data can only make admission fail closed.
|
|
62
|
+
*/
|
|
63
|
+
export class V2SubagentOriginRegistry {
|
|
64
|
+
private readonly entries = new Map<string, StoredV2SubagentOrigin>()
|
|
65
|
+
|
|
66
|
+
capture(rawEvent: unknown, now = Date.now()): V2SubagentOrigin | undefined {
|
|
67
|
+
this.prune(now)
|
|
68
|
+
if (rawEvent === null || typeof rawEvent !== 'object' || Array.isArray(rawEvent)) {
|
|
69
|
+
return undefined
|
|
70
|
+
}
|
|
71
|
+
const event = rawEvent as {
|
|
72
|
+
tool?: unknown
|
|
73
|
+
sessionID?: unknown
|
|
74
|
+
messageID?: unknown
|
|
75
|
+
id?: unknown
|
|
76
|
+
agent?: unknown
|
|
77
|
+
input?: unknown
|
|
78
|
+
}
|
|
79
|
+
if (event.tool !== 'subagent' || event.input === null || typeof event.input !== 'object') {
|
|
80
|
+
return undefined
|
|
81
|
+
}
|
|
82
|
+
const input = event.input as { agent?: unknown }
|
|
83
|
+
const sessionID = nonEmptyString(event.sessionID)
|
|
84
|
+
const messageID = nonEmptyString(event.messageID)
|
|
85
|
+
const callID = nonEmptyString(event.id)
|
|
86
|
+
const caller = agentName(event.agent)
|
|
87
|
+
const target = agentName(input.agent)
|
|
88
|
+
if (
|
|
89
|
+
sessionID === undefined ||
|
|
90
|
+
messageID === undefined ||
|
|
91
|
+
callID === undefined ||
|
|
92
|
+
caller === undefined ||
|
|
93
|
+
target === undefined ||
|
|
94
|
+
!PANTHEON_AGENTS.has(target) ||
|
|
95
|
+
this.entries.size >= MAX_PENDING_ORIGINS
|
|
96
|
+
) {
|
|
97
|
+
return undefined
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const origin = { sessionID, messageID, callID, caller, target }
|
|
101
|
+
this.entries.set(originKey(sessionID, messageID, callID), { ...origin, capturedAt: now })
|
|
102
|
+
return origin
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Remove and return the matching origin, so one tool event authorizes once. */
|
|
106
|
+
consume(event: V2PermissionEvent, now = Date.now()): V2SubagentOrigin | undefined {
|
|
107
|
+
this.prune(now)
|
|
108
|
+
const source = event.source as { type?: unknown; messageID?: unknown; id?: unknown } | undefined
|
|
109
|
+
if (source?.type !== 'tool') return undefined
|
|
110
|
+
const sessionID = nonEmptyString(event.sessionID)
|
|
111
|
+
const messageID = nonEmptyString(source.messageID)
|
|
112
|
+
const callID = nonEmptyString(source.id)
|
|
113
|
+
if (sessionID === undefined || messageID === undefined || callID === undefined) return undefined
|
|
114
|
+
const key = originKey(sessionID, messageID, callID)
|
|
115
|
+
const origin = this.entries.get(key)
|
|
116
|
+
this.entries.delete(key)
|
|
117
|
+
if (origin === undefined) return undefined
|
|
118
|
+
const { capturedAt: _capturedAt, ...result } = origin
|
|
119
|
+
return result
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Cleanup when a subagent tool exits without reaching permission.evaluate. */
|
|
123
|
+
forget(rawEvent: unknown): void {
|
|
124
|
+
if (rawEvent === null || typeof rawEvent !== 'object' || Array.isArray(rawEvent)) return
|
|
125
|
+
const event = rawEvent as {
|
|
126
|
+
tool?: unknown
|
|
127
|
+
sessionID?: unknown
|
|
128
|
+
messageID?: unknown
|
|
129
|
+
id?: unknown
|
|
130
|
+
}
|
|
131
|
+
if (event.tool !== 'subagent') return
|
|
132
|
+
const sessionID = nonEmptyString(event.sessionID)
|
|
133
|
+
const messageID = nonEmptyString(event.messageID)
|
|
134
|
+
const callID = nonEmptyString(event.id)
|
|
135
|
+
if (sessionID === undefined || messageID === undefined || callID === undefined) return
|
|
136
|
+
this.entries.delete(originKey(sessionID, messageID, callID))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private prune(now: number): void {
|
|
140
|
+
for (const [key, origin] of this.entries) {
|
|
141
|
+
if (now - origin.capturedAt > ORIGIN_TTL_MS) this.entries.delete(key)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface AuthoritativeSession {
|
|
147
|
+
id?: unknown
|
|
148
|
+
parentID?: unknown
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export type V2DelegationDecision = 'allow' | 'deny' | 'passthrough' | 'preserve-deny'
|
|
152
|
+
|
|
153
|
+
function agentName(value: unknown): string | undefined {
|
|
154
|
+
if (typeof value !== 'string') return undefined
|
|
155
|
+
const valueNormalized = value.trim().toLowerCase()
|
|
156
|
+
return valueNormalized.length === 0 ? undefined : valueNormalized
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Extract the target from the host's resource string or `{ target }` entry. */
|
|
160
|
+
export function v2ResourceTarget(resource: unknown): string | undefined {
|
|
161
|
+
if (typeof resource === 'string') return agentName(resource)
|
|
162
|
+
if (resource === null || typeof resource !== 'object' || Array.isArray(resource)) return undefined
|
|
163
|
+
return agentName((resource as { target?: unknown }).target)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Decide whether a permission evaluation is a Pantheon-managed delegation.
|
|
168
|
+
* Native/custom-to-native/custom calls are left entirely to host permissions.
|
|
169
|
+
*/
|
|
170
|
+
export function evaluateV2Delegation(
|
|
171
|
+
event: V2PermissionEvent,
|
|
172
|
+
session: AuthoritativeSession | undefined,
|
|
173
|
+
existingStatus?: unknown,
|
|
174
|
+
origin?: V2SubagentOrigin,
|
|
175
|
+
): V2DelegationDecision {
|
|
176
|
+
if (existingStatus === 'deny') return 'preserve-deny'
|
|
177
|
+
|
|
178
|
+
if (!Array.isArray(event.resources) || event.resources.length === 0) return 'deny'
|
|
179
|
+
const rawTargets = event.resources.map(v2ResourceTarget)
|
|
180
|
+
if (rawTargets.some((target) => target === undefined)) return 'deny'
|
|
181
|
+
const targets = rawTargets.filter((target): target is string => target !== undefined)
|
|
182
|
+
const pantheonTargets = targets.filter((target) => PANTHEON_AGENTS.has(target))
|
|
183
|
+
if (pantheonTargets.length === 0) return 'passthrough'
|
|
184
|
+
if (event.action !== 'subagent' || targets.length !== 1 || origin === undefined) return 'deny'
|
|
185
|
+
const sessionID = typeof event.sessionID === 'string' ? event.sessionID : undefined
|
|
186
|
+
const caller = agentName(origin.caller)
|
|
187
|
+
if (
|
|
188
|
+
sessionID === undefined ||
|
|
189
|
+
session === undefined ||
|
|
190
|
+
session.id !== sessionID ||
|
|
191
|
+
caller === undefined ||
|
|
192
|
+
origin.sessionID !== sessionID ||
|
|
193
|
+
origin.target !== pantheonTargets[0]
|
|
194
|
+
) {
|
|
195
|
+
return 'deny'
|
|
196
|
+
}
|
|
197
|
+
const source = event.source as { type?: unknown; messageID?: unknown; id?: unknown } | undefined
|
|
198
|
+
if (
|
|
199
|
+
source?.type !== 'tool' ||
|
|
200
|
+
source.messageID !== origin.messageID ||
|
|
201
|
+
source.id !== origin.callID
|
|
202
|
+
) {
|
|
203
|
+
return 'deny'
|
|
204
|
+
}
|
|
205
|
+
const eventCaller = agentName(event.agent)
|
|
206
|
+
if (eventCaller !== undefined && eventCaller !== caller) return 'deny'
|
|
207
|
+
if (session.parentID !== undefined && session.parentID !== null) {
|
|
208
|
+
return 'deny'
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const callerMayTarget = (target: string): boolean =>
|
|
212
|
+
caller === 'zeus' || ((caller === 'athena' || caller === 'hermes') && target === 'apollo')
|
|
213
|
+
return pantheonTargets.every(callerMayTarget) ? 'allow' : 'deny'
|
|
214
|
+
}
|
|
@@ -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
|
+
}
|