talon-agent 3.33.0 → 3.33.2
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/package.json +3 -3
- package/src/backend/kilo/factory.ts +39 -119
- package/src/backend/kilo/handler/index.ts +1 -11
- package/src/backend/kilo/handler/message.ts +20 -317
- package/src/backend/kilo/server.ts +55 -272
- package/src/backend/kilo/sessions.ts +5 -83
- package/src/backend/opencode/factory.ts +41 -118
- package/src/backend/opencode/handler/index.ts +1 -11
- package/src/backend/opencode/handler/message.ts +20 -321
- package/src/backend/opencode/server.ts +57 -217
- package/src/backend/opencode/sessions.ts +5 -83
- package/src/backend/remote-server/chat-turn.ts +359 -0
- package/src/backend/remote-server/factory.ts +150 -0
- package/src/backend/remote-server/index.ts +14 -19
- package/src/backend/remote-server/server-bindings.ts +232 -0
- package/src/backend/{kilo/handler → remote-server}/turn.ts +103 -67
- package/src/core/mesh/bridge-links.ts +319 -0
- package/src/core/mesh/common.ts +30 -0
- package/src/core/mesh/device-files.ts +710 -0
- package/src/core/mesh/service.ts +72 -894
- package/src/frontend/discord/handlers/access.ts +7 -56
- package/src/frontend/discord/handlers/state.ts +15 -8
- package/src/frontend/native/server.ts +391 -323
- package/src/frontend/shared/access.ts +152 -0
- package/src/frontend/telegram/handlers/access.ts +8 -25
- package/src/frontend/telegram/handlers/queue.ts +3 -35
- package/src/frontend/telegram/handlers/state.ts +15 -8
- package/src/backend/kilo/events.ts +0 -60
- package/src/backend/kilo/handler/state.ts +0 -9
- package/src/backend/opencode/handler/state.ts +0 -9
- package/src/backend/opencode/handler/turn.ts +0 -314
|
@@ -1,314 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Run one OpenCode turn over the HTTP API + SSE event stream.
|
|
3
|
-
*
|
|
4
|
-
* `runOpenCodeTurn` subscribes to SSE before issuing the prompt (so no early
|
|
5
|
-
* events are lost), fires `promptAsync`, awaits the SSE close, then reads the
|
|
6
|
-
* authoritative parts list. `subscribeToTurnEvents` translates relevant SSE
|
|
7
|
-
* events into shared stream-state mutations and observes natural turn idle.
|
|
8
|
-
*
|
|
9
|
-
* The streaming + event-processing logic is shared with the Kilo backend via
|
|
10
|
-
* `backend/remote-server/events.ts`.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import type { OpencodeClient } from "@opencode-ai/sdk/v2";
|
|
14
|
-
import { logWarn } from "../../../util/log.js";
|
|
15
|
-
import { errMsg } from "../server.js";
|
|
16
|
-
import {
|
|
17
|
-
approvePendingPermissions,
|
|
18
|
-
extractPartsSummary,
|
|
19
|
-
extractAssistantUsage,
|
|
20
|
-
rejectPendingQuestions,
|
|
21
|
-
type OpenCodeAssistantInfo,
|
|
22
|
-
} from "../sessions.js";
|
|
23
|
-
import { createStreamState, recordTokens, sleep } from "../../shared/index.js";
|
|
24
|
-
import {
|
|
25
|
-
processStreamEvent,
|
|
26
|
-
finalizePartsIntoState,
|
|
27
|
-
} from "../../remote-server/events.js";
|
|
28
|
-
import { subscribeSseStream } from "../../remote-server/sse-stream.js";
|
|
29
|
-
import { awaitRemoteTurn } from "../../remote-server/turn-timeout.js";
|
|
30
|
-
import { findLastAssistantMessage as findLastAssistantMessageShared } from "../../remote-server/messages.js";
|
|
31
|
-
|
|
32
|
-
export interface RunOpenCodeTurnInputs {
|
|
33
|
-
oc: OpencodeClient;
|
|
34
|
-
sessionId: string;
|
|
35
|
-
prompt: string;
|
|
36
|
-
systemPrompt: string;
|
|
37
|
-
providerID: string;
|
|
38
|
-
modelID: string;
|
|
39
|
-
state: ReturnType<typeof createStreamState>;
|
|
40
|
-
chatId: string;
|
|
41
|
-
seenQuestionIds: Set<string>;
|
|
42
|
-
seenPermissionIds: Set<string>;
|
|
43
|
-
seenToolCallIds: Set<string>;
|
|
44
|
-
toolOverrides?: Record<string, boolean>;
|
|
45
|
-
onStreamDelta?: (accumulated: string, phase?: "thinking" | "text") => void;
|
|
46
|
-
onTextBlock?: (text: string) => Promise<void>;
|
|
47
|
-
onToolUse?: (toolName: string, input: Record<string, unknown>) => void;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Run one OpenCode turn end-to-end. SSE-first strategy identical to Kilo:
|
|
52
|
-
* subscribe before prompting, fire `promptAsync`, await the SSE close, then
|
|
53
|
-
* read the authoritative parts list. Avoids the sync `session.prompt` which
|
|
54
|
-
* would hang our await when the upstream model stalls.
|
|
55
|
-
*/
|
|
56
|
-
export async function runOpenCodeTurn(
|
|
57
|
-
inputs: RunOpenCodeTurnInputs,
|
|
58
|
-
): Promise<void> {
|
|
59
|
-
const {
|
|
60
|
-
oc,
|
|
61
|
-
sessionId,
|
|
62
|
-
prompt,
|
|
63
|
-
systemPrompt,
|
|
64
|
-
providerID,
|
|
65
|
-
modelID,
|
|
66
|
-
state,
|
|
67
|
-
chatId,
|
|
68
|
-
seenQuestionIds,
|
|
69
|
-
seenPermissionIds,
|
|
70
|
-
seenToolCallIds,
|
|
71
|
-
toolOverrides,
|
|
72
|
-
onStreamDelta,
|
|
73
|
-
onTextBlock,
|
|
74
|
-
onToolUse,
|
|
75
|
-
} = inputs;
|
|
76
|
-
|
|
77
|
-
// SSE subscription FIRST — early `session.turn.open` and
|
|
78
|
-
// `message.part.updated` events can fire immediately after promptAsync
|
|
79
|
-
// returns, so the iterator must already be alive.
|
|
80
|
-
const sseAbort = new AbortController();
|
|
81
|
-
const sseDone = subscribeToTurnEvents({
|
|
82
|
-
oc,
|
|
83
|
-
sessionId,
|
|
84
|
-
state,
|
|
85
|
-
chatId,
|
|
86
|
-
seenToolCallIds,
|
|
87
|
-
onStreamDelta,
|
|
88
|
-
onTextBlock,
|
|
89
|
-
onToolUse,
|
|
90
|
-
abortSignal: sseAbort.signal,
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// Headless-interaction watchdog: resolve upstream questions and any
|
|
94
|
-
// permission requests that escaped the session ruleset.
|
|
95
|
-
const questionWatchdog = (async () => {
|
|
96
|
-
while (!sseAbort.signal.aborted) {
|
|
97
|
-
try {
|
|
98
|
-
await Promise.all([
|
|
99
|
-
rejectPendingQuestions(oc, sessionId, chatId, seenQuestionIds),
|
|
100
|
-
approvePendingPermissions(oc, sessionId, chatId, seenPermissionIds),
|
|
101
|
-
]);
|
|
102
|
-
} catch (err) {
|
|
103
|
-
logWarn(
|
|
104
|
-
"agent",
|
|
105
|
-
`[${chatId}] question watchdog failed: ${errMsg(err)}`,
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
await sleep(350, sseAbort.signal);
|
|
109
|
-
}
|
|
110
|
-
})();
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
// Fire and forget — promptAsync returns immediately. The await below
|
|
114
|
-
// is on the SSE close event.
|
|
115
|
-
await awaitRemoteTurn(
|
|
116
|
-
(async () => {
|
|
117
|
-
await oc.session.promptAsync({
|
|
118
|
-
sessionID: sessionId,
|
|
119
|
-
parts: [{ type: "text", text: prompt }],
|
|
120
|
-
model: { providerID, modelID },
|
|
121
|
-
system: systemPrompt,
|
|
122
|
-
...(toolOverrides ? { tools: toolOverrides } : {}),
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
// Await turn completion via SSE.
|
|
126
|
-
await sseDone;
|
|
127
|
-
})(),
|
|
128
|
-
{ client: oc, sessionId, chatId, label: "OpenCode" },
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
// Read authoritative final state from the messages endpoint.
|
|
132
|
-
const messagesResp = await oc.session.messages({ sessionID: sessionId });
|
|
133
|
-
const messages = Array.isArray(messagesResp.data)
|
|
134
|
-
? (messagesResp.data as Array<Record<string, unknown>>)
|
|
135
|
-
: [];
|
|
136
|
-
const lastAssistant = findLastAssistantMessage(messages);
|
|
137
|
-
const parts = lastAssistant?.parts ?? [];
|
|
138
|
-
const assistantInfo = lastAssistant?.info;
|
|
139
|
-
|
|
140
|
-
finalizePartsIntoState({
|
|
141
|
-
parts,
|
|
142
|
-
state,
|
|
143
|
-
seenToolCallIds,
|
|
144
|
-
extractPartsSummary,
|
|
145
|
-
onToolUse,
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
if (assistantInfo) {
|
|
149
|
-
const usage = extractAssistantUsage(assistantInfo);
|
|
150
|
-
if (state.sdkInputTokens === 0 && state.sdkOutputTokens === 0) {
|
|
151
|
-
recordTokens(state, {
|
|
152
|
-
inputTokens: usage.inputTokens,
|
|
153
|
-
outputTokens: usage.outputTokens,
|
|
154
|
-
cacheRead: usage.cacheRead,
|
|
155
|
-
cacheWrite: usage.cacheWrite,
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
} catch (err) {
|
|
160
|
-
// Explicit user interrupts abort intentionally — swallow that close path.
|
|
161
|
-
if (state.turnTerminated && /abort/i.test(errMsg(err))) {
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
throw err;
|
|
165
|
-
} finally {
|
|
166
|
-
sseAbort.abort();
|
|
167
|
-
// A dead SSE socket may ignore the local abort flag until another event
|
|
168
|
-
// arrives. Bound cleanup so a timed-out turn cannot wedge its caller in
|
|
169
|
-
// the finally block it was meant to escape.
|
|
170
|
-
await Promise.race([sseDone.catch(() => {}), sleep(1_000)]);
|
|
171
|
-
await questionWatchdog.catch(() => {});
|
|
172
|
-
try {
|
|
173
|
-
await Promise.all([
|
|
174
|
-
rejectPendingQuestions(oc, sessionId, chatId, seenQuestionIds),
|
|
175
|
-
approvePendingPermissions(oc, sessionId, chatId, seenPermissionIds),
|
|
176
|
-
]);
|
|
177
|
-
} catch {
|
|
178
|
-
/* noop */
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Find the most recent assistant message in a session-messages list and
|
|
185
|
-
* surface its parts + assistant info. Shared walker — see
|
|
186
|
-
* `remote-server/messages.ts`.
|
|
187
|
-
*/
|
|
188
|
-
const findLastAssistantMessage = (
|
|
189
|
-
messages: Array<Record<string, unknown>>,
|
|
190
|
-
): {
|
|
191
|
-
parts: Array<Record<string, unknown>>;
|
|
192
|
-
info?: OpenCodeAssistantInfo;
|
|
193
|
-
} | null => findLastAssistantMessageShared<OpenCodeAssistantInfo>(messages);
|
|
194
|
-
|
|
195
|
-
interface SubscribeInputs {
|
|
196
|
-
oc: OpencodeClient;
|
|
197
|
-
sessionId: string;
|
|
198
|
-
state: ReturnType<typeof createStreamState>;
|
|
199
|
-
chatId: string;
|
|
200
|
-
seenToolCallIds: Set<string>;
|
|
201
|
-
onStreamDelta?: (accumulated: string, phase?: "thinking" | "text") => void;
|
|
202
|
-
onTextBlock?: (text: string) => Promise<void>;
|
|
203
|
-
onToolUse?: (toolName: string, input: Record<string, unknown>) => void;
|
|
204
|
-
abortSignal: AbortSignal;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Subscribe to OpenCode's global SSE event stream and translate relevant
|
|
209
|
-
* events into stream-state mutations / callback firings. Only events scoped
|
|
210
|
-
* to our `sessionId` are processed; others are dropped silently. Errors are
|
|
211
|
-
* logged but do not propagate.
|
|
212
|
-
*/
|
|
213
|
-
async function subscribeToTurnEvents(inputs: SubscribeInputs): Promise<void> {
|
|
214
|
-
const {
|
|
215
|
-
oc,
|
|
216
|
-
sessionId,
|
|
217
|
-
state,
|
|
218
|
-
chatId,
|
|
219
|
-
seenToolCallIds,
|
|
220
|
-
onStreamDelta,
|
|
221
|
-
onTextBlock,
|
|
222
|
-
onToolUse,
|
|
223
|
-
abortSignal,
|
|
224
|
-
} = inputs;
|
|
225
|
-
|
|
226
|
-
// `subscribeSseStream` handles the narrowing with a runtime guard + the
|
|
227
|
-
// subscribe-failed warning.
|
|
228
|
-
const stream = await subscribeSseStream(oc, chatId);
|
|
229
|
-
if (!stream) throw new Error("OpenCode SSE connection unavailable");
|
|
230
|
-
|
|
231
|
-
try {
|
|
232
|
-
for await (const evt of stream) {
|
|
233
|
-
if (abortSignal.aborted) break;
|
|
234
|
-
if (!evt || typeof evt !== "object") continue;
|
|
235
|
-
|
|
236
|
-
// OpenCode's SSE wire format wraps every event in
|
|
237
|
-
// `{payload: {type, properties}}` — same as Kilo.
|
|
238
|
-
const payload =
|
|
239
|
-
evt && typeof evt === "object" && "payload" in evt
|
|
240
|
-
? (evt as { payload?: unknown }).payload
|
|
241
|
-
: evt;
|
|
242
|
-
if (!payload || typeof payload !== "object") continue;
|
|
243
|
-
const event = payload as {
|
|
244
|
-
type?: string;
|
|
245
|
-
properties?: Record<string, unknown>;
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
// session.error scope-filter to our own sessionId before attributing
|
|
249
|
-
// the error to this chat — the SSE stream is global and a heartbeat
|
|
250
|
-
// session.error would otherwise pollute the chat's log.
|
|
251
|
-
if (event.type === "session.error") {
|
|
252
|
-
const props = event.properties ?? {};
|
|
253
|
-
const evtSessionID =
|
|
254
|
-
typeof props.sessionID === "string" ? props.sessionID : undefined;
|
|
255
|
-
if (evtSessionID && evtSessionID !== sessionId) {
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
const errProp = props.error as
|
|
259
|
-
| {
|
|
260
|
-
name?: string;
|
|
261
|
-
message?: string;
|
|
262
|
-
data?: Record<string, unknown>;
|
|
263
|
-
}
|
|
264
|
-
| undefined;
|
|
265
|
-
// MessageAbortedError is expected for an explicit user interrupt.
|
|
266
|
-
const isOurAbort =
|
|
267
|
-
state.turnTerminated &&
|
|
268
|
-
(errProp?.name === "MessageAbortedError" ||
|
|
269
|
-
/abort/i.test(errProp?.name ?? "") ||
|
|
270
|
-
/abort/i.test(errProp?.message ?? ""));
|
|
271
|
-
if (errProp && !isOurAbort) {
|
|
272
|
-
const detail = [
|
|
273
|
-
errProp.name && `name=${errProp.name}`,
|
|
274
|
-
errProp.message && `message=${errProp.message}`,
|
|
275
|
-
errProp.data && `data=${JSON.stringify(errProp.data)}`,
|
|
276
|
-
]
|
|
277
|
-
.filter(Boolean)
|
|
278
|
-
.join(" ");
|
|
279
|
-
logWarn("agent", `[${chatId}] OpenCode session.error: ${detail}`);
|
|
280
|
-
const msg = errProp.message ?? errProp.name;
|
|
281
|
-
if (msg) state.syntheticError = msg;
|
|
282
|
-
}
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const outcome = await processStreamEvent(event, {
|
|
287
|
-
chatId,
|
|
288
|
-
sessionId,
|
|
289
|
-
state,
|
|
290
|
-
seenToolCallIds,
|
|
291
|
-
backendLabel: "OpenCode",
|
|
292
|
-
onStreamDelta,
|
|
293
|
-
onTextBlock,
|
|
294
|
-
onToolUse,
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
if (outcome.kind === "terminator_fired") {
|
|
298
|
-
// Aborting here can race with a prompt accepted immediately after
|
|
299
|
-
// this turn settles and cancel that next turn on the reused session.
|
|
300
|
-
// Delivery is already complete, so wait for natural idle instead.
|
|
301
|
-
continue;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
if (outcome.kind === "stop") {
|
|
305
|
-
if (outcome.reason === "out_of_scope") continue;
|
|
306
|
-
return; // turn.close or idle — stop iterating
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
} catch (err) {
|
|
310
|
-
if (!abortSignal.aborted) {
|
|
311
|
-
logWarn("agent", `[${chatId}] SSE iteration failed: ${errMsg(err)}`);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|