forked from claude-code-best/claude-code
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathports.ts
More file actions
202 lines (196 loc) · 6.95 KB
/
Copy pathports.ts
File metadata and controls
202 lines (196 loc) · 6.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
import {
createFileJournalStore,
type ProgressEvent,
type WorkflowPorts,
} from '@claude-code-best/workflow-engine'
import { logForDebugging } from '../utils/debug.js'
import { getProjectRoot } from '../bootstrap/state.js'
import { getRunsDir } from './persistence.js'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from '../services/analytics/index.js'
import {
completeWorkflowTask,
failWorkflowTask,
killWorkflowTask,
registerLocalWorkflowTask,
} from '../tasks/LocalWorkflowTask/LocalWorkflowTask.js'
import {
buildHostBundle,
makeHostHandle,
readHostBundle,
type WorkflowHostBundle,
} from './hostHandle.js'
import { buildRegistry } from './registry.js'
import type { ProgressBus } from './progress/bus.js'
import type { ProgressStore } from './progress/store.js'
import type { SetAppState } from '../Task.js'
import type { AssistantMessage } from '../types/message.js'
type RunBinding = {
runId: string
taskId: string
setAppState: SetAppState
abortController: AbortController
workflowName: string
/** agentId → AbortController. Registered when backend starts an agent; killAgent uses it for precise abort. */
agentAbortControllers: Map<number, AbortController>
}
/** Constructs a WorkflowHostContext from toolUseContext on each tool invocation. */
function makeHostFactory(): WorkflowPorts['hostFactory'] {
return ({ context, canUseTool, parentMessage }) => {
const ctx = context as WorkflowHostBundle['toolUseContext'] & {
agentId?: string
}
return {
handle: makeHostHandle(
buildHostBundle(
ctx,
canUseTool as WorkflowHostBundle['canUseTool'],
parentMessage as AssistantMessage | undefined,
),
),
// Use projectRoot rather than getCwd(): shares the same root as journalStore's runsDir,
// otherwise named workflow resolution and journal persistence diverge when the user
// enters a worktree/sub-directory. The engine's internal ctx.cwd is only used for
// resolution (scriptPath/name) and does not affect the agent's execution cwd
// (the agent gets its own cwd via the toolUseContext inside the host bundle).
cwd: getProjectRoot(),
budgetTotal: null, // turn-level budget injection point (read from settings in the future)
...(ctx.toolUseId ? { toolUseId: ctx.toolUseId } : {}),
}
}
}
/**
* Assembles the complete WorkflowPorts. bus/store are passed in by the caller (shared via the service singleton).
* taskRegistrar maintains runId → RunBinding for kill routing.
*/
export function createWorkflowPorts(opts: {
bus: ProgressBus
store: ProgressStore
}): WorkflowPorts {
const bindings = new Map<string, RunBinding>()
const runsDir = getRunsDir()
const registry = buildRegistry()
// Telemetry subscription (independent of store). LogEventMetadata only accepts boolean/number/undefined,
// and runId is a string — use the brand cast provided by the analytics module (verified non-code/path) to pass it through.
opts.bus.subscribe((e: ProgressEvent) => {
if (e.type === 'run_done') {
logEvent('tengu_workflow_done', {
status: e.status === 'completed' ? 0 : e.status === 'failed' ? 1 : 2,
runId:
e.runId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
}
})
const taskRegistrar: WorkflowPorts['taskRegistrar'] = {
register(regOpts, host) {
const bundle = readHostBundle(host)
const setAppState =
bundle.toolUseContext.setAppStateForTasks ??
bundle.toolUseContext.setAppState
const abortController = new AbortController()
const taskId = registerLocalWorkflowTask(setAppState, {
description: regOpts.summary ?? regOpts.workflowName,
workflowName: regOpts.workflowName,
workflowFile: regOpts.workflowFile ?? '',
summary: regOpts.summary,
...(regOpts.toolUseId ? { toolUseId: regOpts.toolUseId } : {}),
abortController,
})
const runId = regOpts.runId ?? taskId
bindings.set(runId, {
runId,
taskId,
setAppState,
abortController,
workflowName: regOpts.workflowName,
agentAbortControllers: new Map(),
})
logForDebugging(
`workflow task registered: ${runId} (${regOpts.workflowName})`,
)
return { runId, signal: abortController.signal }
},
complete(runId, summary) {
const b = bindings.get(runId)
if (!b) return
completeWorkflowTask(b.taskId, b.setAppState)
logForDebugging(`workflow ${runId} completed: ${summary ?? ''}`)
bindings.delete(runId)
},
fail(runId, error) {
const b = bindings.get(runId)
if (!b) return
failWorkflowTask(b.taskId, b.setAppState, error)
logForDebugging(`workflow ${runId} failed: ${error}`)
bindings.delete(runId)
},
kill(runId) {
const b = bindings.get(runId)
if (!b) return
killWorkflowTask(b.taskId, b.setAppState) // internal abort controller
// Killing the run also aborts all in-flight agents (guards against the edge timing where the backend misses the task abort)
for (const ac of b.agentAbortControllers.values()) {
try {
ac.abort()
} catch {
// no-op: abort won't throw internally, but fail-closed
}
}
b.agentAbortControllers.clear()
bindings.delete(runId)
},
registerAgentAbort(runId, agentId, ac) {
const b = bindings.get(runId)
if (!b) return
b.agentAbortControllers.set(agentId, ac)
},
unregisterAgentAbort(runId, agentId) {
const b = bindings.get(runId)
if (!b) return
b.agentAbortControllers.delete(agentId)
},
killAgent(runId, agentId) {
const b = bindings.get(runId)
if (!b) return false
const ac = b.agentAbortControllers.get(agentId)
if (!ac) return false
try {
ac.abort()
} catch {
// no-op
}
b.agentAbortControllers.delete(agentId)
return true
},
pendingAction() {
return null // v1: skip/retry not wired (seam retained)
},
}
return {
hostFactory: makeHostFactory(),
agentAdapterRegistry: registry,
agentRunner: {
// Dead-code fallback: hooks always go through agentAdapterRegistry (required on ports). Reaching here means the registry was not registered — fail-fast.
async runAgentToResult() {
throw new Error(
'workflow agentRunner fallback reached — agentAdapterRegistry must be set on ports',
)
},
},
progressEmitter: {
emit(event) {
opts.bus.emit(event) // → store reducer + telemetry
},
},
taskRegistrar,
journalStore: createFileJournalStore(runsDir),
permissionGate: { isAborted: () => false }, // engine uses ctx.signal to check abort
logger: {
debug: msg => logForDebugging(msg),
warn: msg => logForDebugging(`[workflow warn] ${msg}`),
event: name => logForDebugging(`workflow event: ${name}`),
},
}
}