pantheon-opencode 1.5.0-beta.13 → 1.5.0-beta.15
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 +59 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/pyproject.toml +2 -5
- package/src/agents/zeus.md +4 -26
- package/src/pantheon/background-job-board.ts +4 -4
- package/src/pantheon/delegate-manager.ts +95 -10
- package/src/pantheon/delegation-classifier.ts +3 -4
- package/src/pantheon/delegation-finalize.ts +69 -7
- package/src/pantheon/delegation.ts +16 -4
- package/src/pantheon/presets.d.mts +0 -20
- package/src/pantheon/presets.mjs +0 -64
- package/src/pantheon/toon-codec.ts +1 -1
- package/src/plugin-v2.ts +4 -6
- package/src/plugin.ts +6 -6
- package/src/plugins/pantheon-hooks.ts +1 -1
- package/src/plugins/tui/dist/tui.d.ts +57 -40
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +145 -54
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
- package/src/plugins/tui/src/index.tsx +245 -83
- package/src/routing.yml +0 -16
- package/scripts/hash_verify.py +0 -192
- package/scripts/test-context-compression.sh +0 -169
- package/src/pantheon/auto-wake.ts +0 -85
- package/src/pantheon/chat-reminders.ts +0 -9
- package/src/pantheon/comment-checker.ts +0 -48
- package/src/pantheon/compaction-assert.ts +0 -160
- package/src/pantheon/compaction-handoff.ts +0 -304
- package/src/pantheon/cost-tracker.ts +0 -108
- package/src/pantheon/dispatch-guard.ts +0 -109
- package/src/pantheon/permission-eval.ts +0 -284
- package/src/pantheon/preemptive-compact.ts +0 -76
- package/src/pantheon/provider-capabilities.ts +0 -221
- package/src/pantheon/provider-stream-tracker.ts +0 -182
- package/src/pantheon/retry-policy.ts +0 -212
- package/src/pantheon/token-opt.ts +0 -689
- package/src/pantheon/v2-events.ts +0 -148
- package/src/pantheon/v2-hooks.ts +0 -287
- package/src/pantheon/v2-tool-definitions.ts +0 -331
- package/src/pantheon/zeus-delegate-with-retry.ts +0 -406
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* V2 Event Handlers — Pantheon event subscriptions for the V2 plugin API.
|
|
3
|
-
*
|
|
4
|
-
* Subscribes to session lifecycle events (created, idle, error, compacted)
|
|
5
|
-
* and routes them to the appropriate subsystem:
|
|
6
|
-
* - session.created → seed session hierarchy + root sessions
|
|
7
|
-
* - session.idle → delegation finalize / goal loop / todo enforcer
|
|
8
|
-
* - session.error → delegation finalize as error
|
|
9
|
-
* - session.compacted → todo preserve / compaction context
|
|
10
|
-
*
|
|
11
|
-
* These handlers are PURE — they depend only on structural interfaces,
|
|
12
|
-
* not on the V1 PluginInput or V2 PluginContext.
|
|
13
|
-
*
|
|
14
|
-
* @module pantheon/v2-events
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import type { BackgroundJobBoard } from './background-job-board.ts'
|
|
18
|
-
import type { FinalizeInput } from './delegation-finalize.ts'
|
|
19
|
-
import type { DelegationEventDeps, DelegationEventLike } from './delegation-notify.ts'
|
|
20
|
-
import { handleDelegationEvent } from './delegation-notify.ts'
|
|
21
|
-
import type { GoalLoopLike, TodoEnforcerLike } from './idle-continuation.ts'
|
|
22
|
-
import { createIdleDispatcher, type IdleDispatcher } from './idle-continuation.ts'
|
|
23
|
-
import { createPantheonLogger } from './logger.ts'
|
|
24
|
-
|
|
25
|
-
const log = createPantheonLogger({ module: 'pantheon-v2-events' })
|
|
26
|
-
|
|
27
|
-
// ─── Types ───────────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Structural session info from session.created events.
|
|
31
|
-
*/
|
|
32
|
-
export interface SessionInfo {
|
|
33
|
-
id: string
|
|
34
|
-
parentID?: string
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Structural event from the V2 event stream.
|
|
39
|
-
*/
|
|
40
|
-
export interface V2SessionEvent {
|
|
41
|
-
type: string
|
|
42
|
-
properties?: Record<string, unknown>
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Dependencies for the V2 event dispatcher.
|
|
47
|
-
*/
|
|
48
|
-
export interface V2EventDeps {
|
|
49
|
-
board: BackgroundJobBoard
|
|
50
|
-
finalize: (childSessionID: string, opts: FinalizeInput) => Promise<unknown>
|
|
51
|
-
goalLoop: GoalLoopLike
|
|
52
|
-
todoEnforcer: TodoEnforcerLike
|
|
53
|
-
/** Callback to register a session in the hierarchy. */
|
|
54
|
-
registerSession?: (info: SessionInfo) => void
|
|
55
|
-
/** Callback to add a session as root. */
|
|
56
|
-
addRootSession?: (sessionID: string) => void
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* V2 Event Dispatcher — routes session events to subsystems.
|
|
61
|
-
*/
|
|
62
|
-
export interface V2EventDispatcher {
|
|
63
|
-
/** Process a session event. Returns true if handled. */
|
|
64
|
-
handleEvent(event: V2SessionEvent): Promise<boolean>
|
|
65
|
-
/** Cleanup subscriptions. */
|
|
66
|
-
dispose(): void
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ─── Implementation ──────────────────────────────────────────────────────
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Create the V2 event dispatcher. Routes session lifecycle events to
|
|
73
|
-
* delegation finalize, goal loop, and todo enforcer.
|
|
74
|
-
*
|
|
75
|
-
* Usage:
|
|
76
|
-
* ```ts
|
|
77
|
-
* const dispatcher = createV2EventDispatcher(deps)
|
|
78
|
-
* // In event subscription loop:
|
|
79
|
-
* for await (const event of ctx.event.subscribe(...)) {
|
|
80
|
-
* await dispatcher.handleEvent(event)
|
|
81
|
-
* }
|
|
82
|
-
* ```
|
|
83
|
-
*/
|
|
84
|
-
export function createV2EventDispatcher(deps: V2EventDeps): V2EventDispatcher {
|
|
85
|
-
const delegationDeps: DelegationEventDeps = {
|
|
86
|
-
board: deps.board,
|
|
87
|
-
finalize: deps.finalize,
|
|
88
|
-
}
|
|
89
|
-
const idleDispatcher: IdleDispatcher = createIdleDispatcher({
|
|
90
|
-
goalLoop: deps.goalLoop,
|
|
91
|
-
todoEnforcer: deps.todoEnforcer,
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
handleEvent: async (event: V2SessionEvent): Promise<boolean> => {
|
|
96
|
-
try {
|
|
97
|
-
// session.created → seed hierarchy + board put-if-absent for native children
|
|
98
|
-
if (event.type === 'session.created') {
|
|
99
|
-
const info = event.properties?.info as SessionInfo | undefined
|
|
100
|
-
if (info) {
|
|
101
|
-
deps.registerSession?.(info)
|
|
102
|
-
if (info.parentID === undefined) {
|
|
103
|
-
deps.addRootSession?.(info.id)
|
|
104
|
-
} else {
|
|
105
|
-
// Native task(background=true) child: register on the board
|
|
106
|
-
// atomically (skip if V1 idle hook or re-dispatch already claimed it).
|
|
107
|
-
try {
|
|
108
|
-
await deps.board.registerLaunchIfAbsent({
|
|
109
|
-
taskID: info.id,
|
|
110
|
-
parentSessionID: info.parentID,
|
|
111
|
-
agent: 'native',
|
|
112
|
-
description: `native child ${info.id}`,
|
|
113
|
-
})
|
|
114
|
-
} catch (err) {
|
|
115
|
-
// The per-agent concurrency limit can refuse the launch. Do not
|
|
116
|
-
// abort the rest of the handler: this child is simply not
|
|
117
|
-
// mirrored on the board, while finalize/idle dispatch for every
|
|
118
|
-
// other event must still run.
|
|
119
|
-
log.warn(`[Pantheon V2] native child ${info.id} not mirrored on board:`, err)
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Delegate to handleDelegationEvent (covers idle + error for board children)
|
|
126
|
-
const delegated = await handleDelegationEvent(event as DelegationEventLike, delegationDeps)
|
|
127
|
-
|
|
128
|
-
// Non-board idle → idle dispatcher (goal loop / todo enforcer)
|
|
129
|
-
if (!delegated && event.type === 'session.idle') {
|
|
130
|
-
const sessionID = event.properties?.sessionID as string | undefined
|
|
131
|
-
if (sessionID) {
|
|
132
|
-
await idleDispatcher.onIdle(sessionID)
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return delegated
|
|
137
|
-
} catch (err) {
|
|
138
|
-
log.error('[Pantheon V2] Event handling failed:', err)
|
|
139
|
-
return false
|
|
140
|
-
}
|
|
141
|
-
},
|
|
142
|
-
|
|
143
|
-
dispose: () => {
|
|
144
|
-
// No persistent subscriptions to clean up — the event stream
|
|
145
|
-
// lifecycle is managed by the caller.
|
|
146
|
-
},
|
|
147
|
-
}
|
|
148
|
-
}
|
package/src/pantheon/v2-hooks.ts
DELETED
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* V2 Session Hooks — Pantheon session hooks for the V2 plugin API.
|
|
3
|
-
*
|
|
4
|
-
* Provides two hook types:
|
|
5
|
-
* - `"prompt"` hook: Vision message interception (paste-and-ask). Intercepts
|
|
6
|
-
* user messages with pasted images and replaces them with text instructions
|
|
7
|
-
* or native vision descriptions.
|
|
8
|
-
* - `"context"` hook: Inject routing policy, active goals, and pending todos
|
|
9
|
-
* into the system context before each LLM call.
|
|
10
|
-
*
|
|
11
|
-
* These handlers are PURE — they depend only on structural interfaces.
|
|
12
|
-
*
|
|
13
|
-
* @module pantheon/v2-hooks
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import type { BackgroundJobBoard } from './background-job-board.ts'
|
|
17
|
-
import { buildCompactionContext } from './delegation-compaction.ts'
|
|
18
|
-
import type { GoalStore } from './goal-loop.ts'
|
|
19
|
-
import { createPantheonLogger } from './logger.ts'
|
|
20
|
-
import type { TodoEnforcer } from './todo-enforcer.ts'
|
|
21
|
-
|
|
22
|
-
const log = createPantheonLogger({ module: 'pantheon-v2-hooks' })
|
|
23
|
-
|
|
24
|
-
// ─── Types ───────────────────────────────────────────────────────────────
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Structural context event passed to the "context" hook.
|
|
28
|
-
* V2 API: `ctx.session.hook("context", (event) => { ... })`
|
|
29
|
-
*/
|
|
30
|
-
export interface V2ContextHookEvent {
|
|
31
|
-
sessionID: string
|
|
32
|
-
// Unknown: host beta may send string, string[], SystemPart objects
|
|
33
|
-
// ({type,text}), mixed arrays, or non-array. Narrow internally (fail-open).
|
|
34
|
-
system: unknown
|
|
35
|
-
generation: {
|
|
36
|
-
temperature?: number
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Structural prompt event passed to the "prompt" hook.
|
|
42
|
-
* V2 API: `ctx.session.hook("prompt", (event) => { ... })`
|
|
43
|
-
*/
|
|
44
|
-
export interface V2PromptHookEvent {
|
|
45
|
-
sessionID: string
|
|
46
|
-
message: unknown
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Dependencies for session hooks.
|
|
51
|
-
*/
|
|
52
|
-
export interface V2HookDeps {
|
|
53
|
-
board: BackgroundJobBoard
|
|
54
|
-
goalStore: GoalStore
|
|
55
|
-
todoEnforcer: TodoEnforcer
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// ─── Context Hook ────────────────────────────────────────────────────────
|
|
59
|
-
|
|
60
|
-
const POLICY_MARKER = '<!-- pantheon-v2-policy -->'
|
|
61
|
-
const ROUTING_POLICY =
|
|
62
|
-
`${POLICY_MARKER}\nFollow Pantheon routing policy: delegate implementation work ` +
|
|
63
|
-
`to the named specialist and do not claim work was performed without verification.`
|
|
64
|
-
|
|
65
|
-
/** Default max compaction items (matching routing.yml background_delegation.max_compaction_items). */
|
|
66
|
-
const DEFAULT_MAX_COMPACTION_ITEMS = 10
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Extract comparable text from a system entry: plain string or
|
|
70
|
-
* SystemPart-like object ({type,text}). Returns null for junk (null/number).
|
|
71
|
-
* Runtime sniffing: pinned SDK (@opencode-ai/plugin 1.18.18) only knows
|
|
72
|
-
* string/string[] — host beta sends objects. Never throws (fail-open).
|
|
73
|
-
*/
|
|
74
|
-
function systemEntryText(entry: unknown): string | null {
|
|
75
|
-
try {
|
|
76
|
-
if (typeof entry === 'string') return entry
|
|
77
|
-
if (
|
|
78
|
-
typeof entry === 'object' &&
|
|
79
|
-
entry !== null &&
|
|
80
|
-
typeof (entry as { text?: unknown }).text === 'string'
|
|
81
|
-
) {
|
|
82
|
-
return (entry as { text: string }).text
|
|
83
|
-
}
|
|
84
|
-
return null
|
|
85
|
-
} catch {
|
|
86
|
-
return null
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function hasPolicyMarker(arr: unknown[]): boolean {
|
|
91
|
-
try {
|
|
92
|
-
return arr.some((s) => {
|
|
93
|
-
const t = systemEntryText(s)
|
|
94
|
-
return t?.includes(POLICY_MARKER) ?? false
|
|
95
|
-
})
|
|
96
|
-
} catch {
|
|
97
|
-
return false
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function usesObjectShape(arr: unknown[]): boolean {
|
|
102
|
-
try {
|
|
103
|
-
return arr.some(
|
|
104
|
-
(s) =>
|
|
105
|
-
typeof s === 'object' && s !== null && typeof (s as { text?: unknown }).text === 'string',
|
|
106
|
-
)
|
|
107
|
-
} catch {
|
|
108
|
-
return false
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function toSystemEntry(text: string, useObject: boolean): string | { type: 'text'; text: string } {
|
|
113
|
-
return useObject ? { type: 'text', text } : text
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Context hook handler — injects Pantheon policy, routing, and compaction
|
|
118
|
-
* state into the system context before each LLM call.
|
|
119
|
-
*
|
|
120
|
-
* Registered via: `ctx.session.hook("context", handler)`
|
|
121
|
-
*/
|
|
122
|
-
export function createV2ContextHookHandler(deps: V2HookDeps) {
|
|
123
|
-
return async (event: V2ContextHookEvent): Promise<void> => {
|
|
124
|
-
try {
|
|
125
|
-
// Beta 19192 hardening (defense-in-depth) + SystemPart sniffing: the
|
|
126
|
-
// host may send string[], SystemPart objects ({type,text}), mixed
|
|
127
|
-
// arrays, a single string, or a non-array system. Preserve the string
|
|
128
|
-
// path behavior; inject object shape when the array contains objects
|
|
129
|
-
// (else host schema validation fails on system[N]). Never throw.
|
|
130
|
-
const rawSystem: unknown = (event as { system?: unknown } | null | undefined)?.system
|
|
131
|
-
let system: unknown[]
|
|
132
|
-
if (typeof rawSystem === 'string') {
|
|
133
|
-
system = rawSystem.includes(POLICY_MARKER) ? [rawSystem] : [rawSystem, ROUTING_POLICY]
|
|
134
|
-
;(event as { system?: unknown }).system = system
|
|
135
|
-
} else if (Array.isArray(rawSystem)) {
|
|
136
|
-
// 1. Inject routing policy if not already present (string OU .text)
|
|
137
|
-
if (!hasPolicyMarker(rawSystem)) {
|
|
138
|
-
rawSystem.push(toSystemEntry(ROUTING_POLICY, usesObjectShape(rawSystem)))
|
|
139
|
-
}
|
|
140
|
-
system = rawSystem
|
|
141
|
-
} else {
|
|
142
|
-
// Non-string, non-array system: ignore silently (fail-open).
|
|
143
|
-
return
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// 2. Inject compaction context (active goals, pending todos, delegations)
|
|
147
|
-
// Convert blocks to the same shape as the host array (object vs string).
|
|
148
|
-
const blocks = await buildCompactionContext(deps.board, {
|
|
149
|
-
sessionID: event.sessionID,
|
|
150
|
-
maxItems: DEFAULT_MAX_COMPACTION_ITEMS,
|
|
151
|
-
goals: {
|
|
152
|
-
enabled: true,
|
|
153
|
-
list: (sessionID: string) => deps.goalStore.list(sessionID),
|
|
154
|
-
},
|
|
155
|
-
todos: {
|
|
156
|
-
enabled: true,
|
|
157
|
-
list: (sessionID: string) => deps.todoEnforcer.listPendingTodos(sessionID),
|
|
158
|
-
},
|
|
159
|
-
})
|
|
160
|
-
if (blocks.length > 0) {
|
|
161
|
-
const useObject = usesObjectShape(system)
|
|
162
|
-
for (const b of blocks) {
|
|
163
|
-
system.push(useObject ? { type: 'text' as const, text: b } : b)
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// 3. Apply conservative temperature for routing decisions
|
|
168
|
-
const generation = (event as { generation?: { temperature?: number } }).generation
|
|
169
|
-
if (generation != null && generation.temperature === undefined) {
|
|
170
|
-
generation.temperature = 0.2
|
|
171
|
-
}
|
|
172
|
-
} catch (err) {
|
|
173
|
-
log.warn('[Pantheon V2] Context hook failed:', err)
|
|
174
|
-
// Fail-open: never break the session
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// ─── Prompt Hook ─────────────────────────────────────────────────────────
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Prompt hook handler — intercepts user messages for vision processing.
|
|
183
|
-
*
|
|
184
|
-
* Registered via: `ctx.session.hook("prompt", handler)`
|
|
185
|
-
*
|
|
186
|
-
* Note: The full vision handler is complex (file I/O, provider API calls).
|
|
187
|
-
* This is a minimal adapter that delegates to the V1 vision handler
|
|
188
|
-
* if available. Without vision infrastructure, it's a no-op.
|
|
189
|
-
*/
|
|
190
|
-
export function createV2PromptHookHandler(_deps: V2HookDeps) {
|
|
191
|
-
return async (event: V2PromptHookEvent): Promise<void> => {
|
|
192
|
-
// Vision interception is a complex subsystem (see src/pantheon/vision.ts).
|
|
193
|
-
// The V2 prompt hook delegates to the V1 vision handler via the plugin's
|
|
194
|
-
// integration point. Without V1 client access, this is a graceful no-op.
|
|
195
|
-
//
|
|
196
|
-
// TODO: Wire full vision handler when V2 provides SDK client access.
|
|
197
|
-
void event
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// ─── Tool Hooks ──────────────────────────────────────────────────────────
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Tool execute.before hook — enforces read-only restrictions, command
|
|
205
|
-
* normalization, and todo preservation.
|
|
206
|
-
*
|
|
207
|
-
* Registered via: `ctx.tool.hook("execute.before", handler)`
|
|
208
|
-
*/
|
|
209
|
-
export interface V2ToolBeforeEvent {
|
|
210
|
-
tool: string
|
|
211
|
-
sessionID: string
|
|
212
|
-
callID: string
|
|
213
|
-
args?: unknown
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
export function createV2ToolBeforeHookHandler(
|
|
217
|
-
enforcementGuard: (
|
|
218
|
-
input: { tool: string; sessionID: string; callID: string },
|
|
219
|
-
output?: { args?: unknown },
|
|
220
|
-
) => void,
|
|
221
|
-
commandNormalizer: (
|
|
222
|
-
input: { tool: string; sessionID: string; callID: string },
|
|
223
|
-
output?: { args?: unknown },
|
|
224
|
-
) => Promise<void>,
|
|
225
|
-
) {
|
|
226
|
-
return async (event: V2ToolBeforeEvent): Promise<void> => {
|
|
227
|
-
try {
|
|
228
|
-
enforcementGuard(
|
|
229
|
-
{ tool: event.tool, sessionID: event.sessionID, callID: event.callID },
|
|
230
|
-
{ args: event.args },
|
|
231
|
-
)
|
|
232
|
-
await commandNormalizer(
|
|
233
|
-
{ tool: event.tool, sessionID: event.sessionID, callID: event.callID },
|
|
234
|
-
{ args: event.args },
|
|
235
|
-
)
|
|
236
|
-
} catch (err) {
|
|
237
|
-
log.warn('[Pantheon V2] Tool before-hook failed:', err)
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Tool execute.after hook — augments read output with hashline tags
|
|
244
|
-
* and applies context sandbox truncation.
|
|
245
|
-
*
|
|
246
|
-
* Registered via: `ctx.tool.hook("execute.after", handler)`
|
|
247
|
-
*/
|
|
248
|
-
export interface V2ToolAfterEvent {
|
|
249
|
-
tool: string
|
|
250
|
-
sessionID: string
|
|
251
|
-
callID: string
|
|
252
|
-
args?: unknown
|
|
253
|
-
// The host may send a non-string output (shape drift on beta). Keep this
|
|
254
|
-
// honest as unknown and narrow with typeof at the use site — never assume.
|
|
255
|
-
output: unknown
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
export function createV2ToolAfterHookHandler(
|
|
259
|
-
readEnhancer: (
|
|
260
|
-
input: { tool: string; sessionID: string; callID: string; args?: unknown },
|
|
261
|
-
output: { output: string },
|
|
262
|
-
) => Promise<void>,
|
|
263
|
-
sandboxHandler: (
|
|
264
|
-
input: { tool: string; sessionID: string; callID: string; args?: unknown },
|
|
265
|
-
output: { output: string },
|
|
266
|
-
) => Promise<void>,
|
|
267
|
-
) {
|
|
268
|
-
return async (event: V2ToolAfterEvent): Promise<void> => {
|
|
269
|
-
try {
|
|
270
|
-
const input = {
|
|
271
|
-
tool: event.tool,
|
|
272
|
-
sessionID: event.sessionID,
|
|
273
|
-
callID: event.callID,
|
|
274
|
-
args: event.args,
|
|
275
|
-
}
|
|
276
|
-
// Narrow the honest unknown: only strings flow through the enhancers.
|
|
277
|
-
// Non-string output is left untouched (fail-open, preserve host shape).
|
|
278
|
-
if (typeof event.output !== 'string') return
|
|
279
|
-
const output = { output: event.output }
|
|
280
|
-
await sandboxHandler(input, output)
|
|
281
|
-
await readEnhancer(input, output)
|
|
282
|
-
event.output = output.output
|
|
283
|
-
} catch (err) {
|
|
284
|
-
log.warn('[Pantheon V2] Tool after-hook failed:', err)
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|