ticlawk 0.1.17-dev.17 → 0.1.17-dev.19
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/README.md +3 -17
- package/bin/ticlawk.mjs +21 -245
- package/package.json +1 -1
- package/src/adapters/ticlawk/api.mjs +51 -327
- package/src/adapters/ticlawk/credentials.mjs +1 -41
- package/src/adapters/ticlawk/index.mjs +27 -249
- package/src/adapters/ticlawk/wake-client.mjs +1 -1
- package/src/cli/agent-commands.mjs +22 -703
- package/src/core/agent-cli-handlers.mjs +18 -519
- package/src/core/agent-home.mjs +1 -64
- package/src/core/events/worker-events.mjs +36 -32
- package/src/core/http.mjs +0 -138
- package/src/core/runtime-contract.mjs +1 -0
- package/src/core/runtime-env.mjs +0 -7
- package/src/core/runtime-support.mjs +78 -130
- package/src/runtimes/_shared/incoming-message-prompt.mjs +232 -0
- package/src/runtimes/_shared/runtime-base-instructions.mjs +34 -0
- package/src/runtimes/claude-code/index.mjs +48 -21
- package/src/runtimes/claude-code/session.mjs +7 -2
- package/src/runtimes/codex/index.mjs +64 -116
- package/src/runtimes/codex/session.mjs +12 -2
- package/src/runtimes/openclaw/index.mjs +30 -17
- package/src/runtimes/opencode/index.mjs +64 -42
- package/src/runtimes/opencode/session.mjs +14 -14
- package/src/runtimes/pi/index.mjs +64 -42
- package/src/runtimes/pi/session.mjs +8 -11
- package/ticlawk.mjs +30 -0
- package/src/runtimes/_shared/agent-handbook.mjs +0 -38
- package/src/runtimes/_shared/brand.mjs +0 -2
- package/src/runtimes/_shared/goal-step-prompt.mjs +0 -133
- package/src/runtimes/_shared/goal-task-protocol.mjs +0 -50
- package/src/runtimes/_shared/standing-prompt.mjs +0 -331
- package/src/runtimes/_shared/wake-prompt.mjs +0 -296
|
@@ -5,10 +5,7 @@
|
|
|
5
5
|
* and discover local sessions.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
10
|
-
import { homedir } from 'node:os';
|
|
11
|
-
import { basename, join } from 'node:path';
|
|
8
|
+
import { basename } from 'node:path';
|
|
12
9
|
import {
|
|
13
10
|
createCodexSession,
|
|
14
11
|
runCodexPrompt,
|
|
@@ -21,97 +18,18 @@ import {
|
|
|
21
18
|
requireCodexPath,
|
|
22
19
|
} from './session.mjs';
|
|
23
20
|
import { buildCodexInputFromInbound } from '../../core/media/inbound.mjs';
|
|
24
|
-
import {
|
|
21
|
+
import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
|
|
25
22
|
import {
|
|
26
23
|
shouldStreamRuntime,
|
|
24
|
+
createDeltaAggregator,
|
|
27
25
|
reportSubprocessFailure,
|
|
28
26
|
terminalRuntimeFailure,
|
|
29
27
|
updateBindingRuntimeMeta,
|
|
30
28
|
isRuntimeGatewayFailure,
|
|
31
|
-
resolveRuntimeSessionScope,
|
|
32
|
-
buildRuntimeSessionMetaPatch,
|
|
33
29
|
} from '../../core/runtime-support.mjs';
|
|
34
30
|
import { buildAgentRuntimeEnv } from '../../core/runtime-env.mjs';
|
|
35
|
-
import {
|
|
31
|
+
import { buildRuntimeBaseInstructions } from '../_shared/runtime-base-instructions.mjs';
|
|
36
32
|
import { ensureAgentHome } from '../../core/agent-home.mjs';
|
|
37
|
-
import { debugError, debugLog } from '../../core/logger.mjs';
|
|
38
|
-
|
|
39
|
-
function parseBooleanish(value) {
|
|
40
|
-
if (value === undefined || value === null || value === '') return false;
|
|
41
|
-
return ['1', 'true', 'on', 'yes'].includes(String(value).trim().toLowerCase());
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function sha256(text) {
|
|
45
|
-
return createHash('sha256').update(String(text || ''), 'utf8').digest('hex');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function fileSafeId(value) {
|
|
49
|
-
return String(value || 'unknown')
|
|
50
|
-
.replace(/[^a-zA-Z0-9_-]+/g, '-')
|
|
51
|
-
.replace(/^-+|-+$/g, '')
|
|
52
|
-
.slice(0, 80) || 'unknown';
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function writePromptSnapshot({
|
|
56
|
-
binding,
|
|
57
|
-
inbound,
|
|
58
|
-
agentHome,
|
|
59
|
-
targetSessionId,
|
|
60
|
-
shouldRotate,
|
|
61
|
-
developerInstructions,
|
|
62
|
-
message,
|
|
63
|
-
input,
|
|
64
|
-
}) {
|
|
65
|
-
if (!parseBooleanish(process.env.TICLAWK_LOG_RUNTIME_PROMPTS)) return;
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
const rootDir = process.env.TICLAWK_RUNTIME_PROMPT_LOG_DIR
|
|
69
|
-
|| join(homedir(), '.ticlawk', 'prompt-logs');
|
|
70
|
-
const dir = join(rootDir, fileSafeId(binding?.id));
|
|
71
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
72
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
73
|
-
const messageId = fileSafeId(inbound?.messageId);
|
|
74
|
-
const filePath = join(dir, `${stamp}-${messageId}.json`);
|
|
75
|
-
const payload = {
|
|
76
|
-
createdAt: new Date().toISOString(),
|
|
77
|
-
runtime: 'codex',
|
|
78
|
-
agentId: binding?.id || null,
|
|
79
|
-
conversationId: inbound?.conversationId || null,
|
|
80
|
-
messageId: inbound?.messageId || null,
|
|
81
|
-
target: inbound?.envelopeTarget || null,
|
|
82
|
-
action: inbound?.action || null,
|
|
83
|
-
sessionId: targetSessionId || null,
|
|
84
|
-
shouldRotate: Boolean(shouldRotate),
|
|
85
|
-
agentHome,
|
|
86
|
-
hashes: {
|
|
87
|
-
developerInstructionsSha256: sha256(developerInstructions),
|
|
88
|
-
messageSha256: sha256(message),
|
|
89
|
-
},
|
|
90
|
-
developerInstructions,
|
|
91
|
-
message,
|
|
92
|
-
input: input || null,
|
|
93
|
-
};
|
|
94
|
-
writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
|
|
95
|
-
debugLog('codex', 'prompt.snapshot', {
|
|
96
|
-
agentId: binding?.id,
|
|
97
|
-
conversationId: inbound?.conversationId,
|
|
98
|
-
messageId: inbound?.messageId,
|
|
99
|
-
target: inbound?.envelopeTarget,
|
|
100
|
-
path: filePath,
|
|
101
|
-
developerInstructionChars: String(developerInstructions || '').length,
|
|
102
|
-
messageChars: String(message || '').length,
|
|
103
|
-
developerInstructionsSha256: payload.hashes.developerInstructionsSha256,
|
|
104
|
-
messageSha256: payload.hashes.messageSha256,
|
|
105
|
-
});
|
|
106
|
-
} catch (err) {
|
|
107
|
-
debugError('codex', 'prompt.snapshot.failed', {
|
|
108
|
-
agentId: binding?.id,
|
|
109
|
-
conversationId: inbound?.conversationId,
|
|
110
|
-
messageId: inbound?.messageId,
|
|
111
|
-
error: err?.message || String(err),
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
33
|
|
|
116
34
|
export const codexRuntime = {
|
|
117
35
|
name: 'codex',
|
|
@@ -222,42 +140,46 @@ export const codexRuntime = {
|
|
|
222
140
|
? (codexInput.find((item) => item?.type === 'text')?.text || inbound.text || '(image attached)')
|
|
223
141
|
: inbound.text;
|
|
224
142
|
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
|
|
143
|
+
const shouldRotate = !meta.sessionId || meta.rotatePending;
|
|
144
|
+
const deltaAggregator = createDeltaAggregator({
|
|
145
|
+
flushDelta: async ({ text, sessionId, turnId, cwd }) => {
|
|
146
|
+
await emitWorkerEvent({
|
|
147
|
+
adapter,
|
|
148
|
+
binding,
|
|
149
|
+
agent: this.name,
|
|
150
|
+
sessionId: sessionId || meta.sessionId || binding.id,
|
|
151
|
+
turnId: turnId || null,
|
|
152
|
+
cwd: cwd || agentHome,
|
|
153
|
+
replyToMessageId: inbound.messageId || null,
|
|
154
|
+
event: {
|
|
155
|
+
hook_event_name: 'worker.message.delta',
|
|
156
|
+
worker_event_name: 'worker.message.delta',
|
|
157
|
+
delta: text,
|
|
158
|
+
},
|
|
159
|
+
logger: ctx.logger,
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
});
|
|
228
163
|
try {
|
|
229
164
|
const runtimeCodexPath = requireCodexPath(meta.codexPath || meta.runtimePath);
|
|
230
165
|
const runtimeCodexVersion = getCodexRuntimeHealth(runtimeCodexPath).version || meta.codexVersion || null;
|
|
231
166
|
const agentEnv = buildAgentRuntimeEnv({
|
|
232
167
|
agentId: binding.id,
|
|
233
|
-
sessionId:
|
|
168
|
+
sessionId: shouldRotate ? null : meta.sessionId,
|
|
234
169
|
hostId: binding.runtime_host_id,
|
|
235
|
-
conversationId: inbound.conversationId,
|
|
236
|
-
messageId: inbound.messageId,
|
|
237
|
-
target: inbound.envelopeTarget,
|
|
238
|
-
});
|
|
239
|
-
const developerInstructions = buildStandingPrompt({ agentId: binding.id, inbound });
|
|
240
|
-
writePromptSnapshot({
|
|
241
|
-
binding,
|
|
242
|
-
inbound,
|
|
243
|
-
agentHome,
|
|
244
|
-
targetSessionId,
|
|
245
|
-
shouldRotate,
|
|
246
|
-
developerInstructions,
|
|
247
|
-
message,
|
|
248
|
-
input: codexInput,
|
|
249
170
|
});
|
|
171
|
+
const developerInstructions = buildRuntimeBaseInstructions({ agentId: binding.id });
|
|
250
172
|
const result = (shouldRotate || inbound.action === 'image' || shouldStreamRuntime(this.name, this))
|
|
251
|
-
? await this.runTurnStream({ sessionId:
|
|
173
|
+
? await this.runTurnStream({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, codexPath: runtimeCodexPath, agentEnv }, message, {
|
|
252
174
|
input: codexInput,
|
|
253
175
|
developerInstructions,
|
|
254
176
|
onEvent: async (event) => {
|
|
255
177
|
if (event?.type === 'turn.started') {
|
|
256
|
-
|
|
178
|
+
await emitWorkerEvent({
|
|
257
179
|
adapter,
|
|
258
180
|
binding,
|
|
259
181
|
agent: this.name,
|
|
260
|
-
sessionId: event.sessionId ||
|
|
182
|
+
sessionId: event.sessionId || meta.sessionId || binding.id,
|
|
261
183
|
turnId: event.turnId || null,
|
|
262
184
|
cwd: agentHome,
|
|
263
185
|
replyToMessageId: inbound.messageId || null,
|
|
@@ -267,27 +189,34 @@ export const codexRuntime = {
|
|
|
267
189
|
},
|
|
268
190
|
logger: ctx.logger,
|
|
269
191
|
});
|
|
192
|
+
} else if (event?.type === 'message.delta' && event.text) {
|
|
193
|
+
deltaAggregator.push(event.text, {
|
|
194
|
+
sessionId: event.sessionId || meta.sessionId || binding.id,
|
|
195
|
+
turnId: event.turnId || null,
|
|
196
|
+
cwd: agentHome,
|
|
197
|
+
});
|
|
270
198
|
}
|
|
271
199
|
},
|
|
272
200
|
})
|
|
273
|
-
: await this.runTurn({ sessionId:
|
|
201
|
+
: await this.runTurn({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, codexPath: runtimeCodexPath, agentEnv }, message, {
|
|
274
202
|
input: codexInput,
|
|
275
203
|
});
|
|
276
204
|
|
|
205
|
+
await deltaAggregator.flush();
|
|
277
206
|
const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
path: result?.path,
|
|
281
|
-
}),
|
|
207
|
+
sessionId: result?.sessionId || meta.sessionId,
|
|
208
|
+
path: result?.path || meta.path || null,
|
|
282
209
|
runtimePath: runtimeCodexPath,
|
|
283
210
|
codexPath: runtimeCodexPath,
|
|
284
211
|
codexVersion: runtimeCodexVersion,
|
|
212
|
+
rotatePending: false,
|
|
213
|
+
lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
|
|
285
214
|
}, { status: 'connected' });
|
|
286
|
-
|
|
215
|
+
await emitWorkerEvent({
|
|
287
216
|
adapter,
|
|
288
217
|
binding: nextBinding,
|
|
289
218
|
agent: this.name,
|
|
290
|
-
sessionId: result?.sessionId ||
|
|
219
|
+
sessionId: result?.sessionId || meta.sessionId || binding.id,
|
|
291
220
|
turnId: result?.turnId || null,
|
|
292
221
|
cwd: result?.cwd || agentHome,
|
|
293
222
|
replyToMessageId: inbound.messageId || null,
|
|
@@ -299,6 +228,7 @@ export const codexRuntime = {
|
|
|
299
228
|
});
|
|
300
229
|
return true;
|
|
301
230
|
} catch (err) {
|
|
231
|
+
await deltaAggregator.flush().catch(() => {});
|
|
302
232
|
const failureInfo = err?.info || {
|
|
303
233
|
ok: false,
|
|
304
234
|
kind: 'exit-error',
|
|
@@ -313,11 +243,11 @@ export const codexRuntime = {
|
|
|
313
243
|
lastGatewayFailureReason: failureInfo.errorMessage || err?.message || 'Codex app-server error',
|
|
314
244
|
}, { status: 'degraded' }).catch(() => binding);
|
|
315
245
|
}
|
|
316
|
-
|
|
246
|
+
await emitWorkerEvent({
|
|
317
247
|
adapter,
|
|
318
248
|
binding: failureBinding,
|
|
319
249
|
agent: this.name,
|
|
320
|
-
sessionId:
|
|
250
|
+
sessionId: meta.sessionId || binding.id,
|
|
321
251
|
turnId: failureInfo?.turnId || null,
|
|
322
252
|
cwd: agentHome,
|
|
323
253
|
replyToMessageId: inbound.messageId || null,
|
|
@@ -339,6 +269,24 @@ export const codexRuntime = {
|
|
|
339
269
|
}
|
|
340
270
|
},
|
|
341
271
|
|
|
272
|
+
async reconcileAfterRestart(binding, ctx) {
|
|
273
|
+
const meta = binding.runtimeMeta || {};
|
|
274
|
+
await emitWorkerEvent({
|
|
275
|
+
adapter: ctx.adapter,
|
|
276
|
+
binding,
|
|
277
|
+
agent: this.name,
|
|
278
|
+
sessionId: meta.sessionId || binding.id,
|
|
279
|
+
cwd: ensureAgentHome(binding.id) || '',
|
|
280
|
+
event: {
|
|
281
|
+
hook_event_name: 'Stop',
|
|
282
|
+
worker_event_name: 'worker.turn.complete',
|
|
283
|
+
reason: 'connector.restart.reconcile',
|
|
284
|
+
},
|
|
285
|
+
logger: ctx.logger,
|
|
286
|
+
});
|
|
287
|
+
return 1;
|
|
288
|
+
},
|
|
289
|
+
|
|
342
290
|
sessionsDir: CODEX_SESSIONS_DIR,
|
|
343
291
|
maxAgeMs: CODEX_MAX_AGE_MS,
|
|
344
292
|
};
|
|
@@ -368,12 +368,14 @@ export function streamCodexPrompt({
|
|
|
368
368
|
let finalText = '';
|
|
369
369
|
let threadPath = null;
|
|
370
370
|
const ignoredChildEventsLogged = new Set();
|
|
371
|
+
let eventChain = Promise.resolve();
|
|
371
372
|
|
|
372
373
|
const emit = (event) => {
|
|
373
374
|
if (typeof onEvent !== 'function') return;
|
|
374
|
-
|
|
375
|
+
eventChain = eventChain
|
|
375
376
|
.then(() => onEvent(event))
|
|
376
377
|
.catch(() => {});
|
|
378
|
+
return eventChain;
|
|
377
379
|
};
|
|
378
380
|
|
|
379
381
|
const settle = (fn, value) => {
|
|
@@ -385,7 +387,9 @@ export function streamCodexPrompt({
|
|
|
385
387
|
}
|
|
386
388
|
pending.clear();
|
|
387
389
|
try { child.kill('SIGTERM'); } catch {}
|
|
388
|
-
|
|
390
|
+
eventChain
|
|
391
|
+
.catch(() => {})
|
|
392
|
+
.finally(() => fn(value));
|
|
389
393
|
};
|
|
390
394
|
|
|
391
395
|
const send = (method, params = {}) => {
|
|
@@ -447,6 +451,12 @@ export function streamCodexPrompt({
|
|
|
447
451
|
const delta = params?.delta;
|
|
448
452
|
if (typeof delta === 'string' && delta && isRootContext(params)) {
|
|
449
453
|
finalText += delta;
|
|
454
|
+
emit({
|
|
455
|
+
type: 'message.delta',
|
|
456
|
+
sessionId: rootThreadId,
|
|
457
|
+
turnId: rootTurnId,
|
|
458
|
+
text: delta,
|
|
459
|
+
});
|
|
450
460
|
} else if (typeof delta === 'string' && delta) {
|
|
451
461
|
logIgnoredChildEvent('codex.non-root-agent-delta', params);
|
|
452
462
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { reportSubprocessFailure, terminalRuntimeFailure } from '../../core/runtime-support.mjs';
|
|
2
|
-
import {
|
|
3
|
-
import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
|
|
2
|
+
import { buildRuntimeBaseInstructions } from '../_shared/runtime-base-instructions.mjs';
|
|
4
3
|
import { GATEWAY_HOST, GATEWAY_PORT } from './identity.mjs';
|
|
5
4
|
|
|
6
5
|
// Cheap availability probe used by the harness picker. We can't use
|
|
@@ -25,12 +24,11 @@ async function probeOpenClawGatewayHealth() {
|
|
|
25
24
|
import { buildOpenClawSessionKey, normalizeOpenClawAgentId } from './target.mjs';
|
|
26
25
|
import { askGateway, isGatewayReady, registerOpenClawChannel } from './gateway.mjs';
|
|
27
26
|
|
|
28
|
-
// Tracks which (agentId, sessionKey
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
const standingPromptSeen = new Set();
|
|
27
|
+
// Tracks which (agentId, sessionKey) pairs already saw the runtime base
|
|
28
|
+
// instructions this process lifetime. OpenClaw's gateway holds session state
|
|
29
|
+
// out of process, so we err on the side of "inject on first observed
|
|
30
|
+
// turn after daemon restart"; the gateway dedupes redundant context.
|
|
31
|
+
const runtimeBaseInstructionsSeen = new Set();
|
|
34
32
|
import { addInFlight, recoverInFlightEntries, removeInFlight } from './inflight.mjs';
|
|
35
33
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
36
34
|
import { extname } from 'node:path';
|
|
@@ -160,16 +158,16 @@ export const openClawRuntime = {
|
|
|
160
158
|
const agentId = normalizeOpenClawAgentId(meta.agentId || binding.id);
|
|
161
159
|
const sessionId = String(meta.sessionKey || buildOpenClawSessionKey(agentId)).trim();
|
|
162
160
|
|
|
163
|
-
// Inject the
|
|
164
|
-
// daemon start for this (agent, session
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
const
|
|
161
|
+
// Inject the runtime base instructions on the first observed turn after a
|
|
162
|
+
// daemon start for this (agent, session) pair. OpenClaw has no
|
|
163
|
+
// separate "system prompt" parameter on the gateway, so we prepend
|
|
164
|
+
// exactly once.
|
|
165
|
+
const runtimeBaseInstructionsKey = `${agentId}|${sessionId}`;
|
|
168
166
|
let prompt = rawPrompt;
|
|
169
|
-
if (!
|
|
170
|
-
const
|
|
171
|
-
prompt = `${
|
|
172
|
-
|
|
167
|
+
if (!runtimeBaseInstructionsSeen.has(runtimeBaseInstructionsKey)) {
|
|
168
|
+
const baseInstructions = buildRuntimeBaseInstructions({ agentId: binding.id });
|
|
169
|
+
prompt = `${baseInstructions}\n\n---\n\n${rawPrompt}`;
|
|
170
|
+
runtimeBaseInstructionsSeen.add(runtimeBaseInstructionsKey);
|
|
173
171
|
}
|
|
174
172
|
|
|
175
173
|
if (inbound.messageId) {
|
|
@@ -238,6 +236,21 @@ export const openClawRuntime = {
|
|
|
238
236
|
return entries.length;
|
|
239
237
|
},
|
|
240
238
|
|
|
239
|
+
async reconcileAfterRestart(binding, ctx) {
|
|
240
|
+
const meta = binding.runtimeMeta || {};
|
|
241
|
+
if (typeof ctx.adapter.emitEvent === 'function') {
|
|
242
|
+
await ctx.adapter.emitEvent(binding, {
|
|
243
|
+
agent: this.name,
|
|
244
|
+
sessionId: String(meta.sessionKey || buildOpenClawSessionKey(meta.agentId || binding.id)).trim(),
|
|
245
|
+
cwd: '',
|
|
246
|
+
event: {
|
|
247
|
+
hook_event_name: 'agent.run.end',
|
|
248
|
+
reason: 'connector.restart.reconcile',
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return 1;
|
|
253
|
+
},
|
|
241
254
|
};
|
|
242
255
|
|
|
243
256
|
export default openClawRuntime;
|
|
@@ -10,8 +10,7 @@
|
|
|
10
10
|
import { existsSync } from 'node:fs';
|
|
11
11
|
import { basename } from 'node:path';
|
|
12
12
|
import { buildAgentRuntimeEnv } from '../../core/runtime-env.mjs';
|
|
13
|
-
import {
|
|
14
|
-
import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
|
|
13
|
+
import { buildRuntimeBaseInstructions } from '../_shared/runtime-base-instructions.mjs';
|
|
15
14
|
import { ensureAgentHome } from '../../core/agent-home.mjs';
|
|
16
15
|
import {
|
|
17
16
|
createOpenCodeSession,
|
|
@@ -25,18 +24,15 @@ import {
|
|
|
25
24
|
requireOpenCodePath,
|
|
26
25
|
} from './session.mjs';
|
|
27
26
|
import { buildOpenCodeInputFromInbound } from '../../core/media/inbound.mjs';
|
|
28
|
-
import {
|
|
27
|
+
import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
|
|
29
28
|
import {
|
|
30
29
|
shouldStreamRuntime,
|
|
30
|
+
createDeltaAggregator,
|
|
31
31
|
reportSubprocessFailure,
|
|
32
32
|
terminalRuntimeFailure,
|
|
33
33
|
updateBindingRuntimeMeta,
|
|
34
|
-
resolveRuntimeSessionScope,
|
|
35
|
-
buildRuntimeSessionMetaPatch,
|
|
36
34
|
} from '../../core/runtime-support.mjs';
|
|
37
35
|
|
|
38
|
-
const standingPromptSeen = new Set();
|
|
39
|
-
|
|
40
36
|
export const openCodeRuntime = {
|
|
41
37
|
name: 'opencode',
|
|
42
38
|
|
|
@@ -51,8 +47,7 @@ export const openCodeRuntime = {
|
|
|
51
47
|
message: text,
|
|
52
48
|
opencodePath,
|
|
53
49
|
agentEnv,
|
|
54
|
-
|
|
55
|
-
forceStandingPrompt: Boolean(opts.forceStandingPrompt),
|
|
50
|
+
runtimeBaseInstructions: opts.runtimeBaseInstructions || null,
|
|
56
51
|
files: opts.files,
|
|
57
52
|
timeoutMs: opts.timeoutMs,
|
|
58
53
|
});
|
|
@@ -65,8 +60,7 @@ export const openCodeRuntime = {
|
|
|
65
60
|
message: text,
|
|
66
61
|
opencodePath,
|
|
67
62
|
agentEnv,
|
|
68
|
-
|
|
69
|
-
forceStandingPrompt: Boolean(opts.forceStandingPrompt),
|
|
63
|
+
runtimeBaseInstructions: opts.runtimeBaseInstructions || null,
|
|
70
64
|
files: opts.files,
|
|
71
65
|
timeoutMs: opts.timeoutMs,
|
|
72
66
|
onEvent: opts.onEvent,
|
|
@@ -162,39 +156,47 @@ export const openCodeRuntime = {
|
|
|
162
156
|
// instruction so it has something to anchor on.
|
|
163
157
|
message = captionText || 'Please analyze the attached image(s).';
|
|
164
158
|
}
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
const
|
|
159
|
+
const shouldRotate = !meta.sessionId || meta.rotatePending;
|
|
160
|
+
|
|
161
|
+
const deltaAggregator = createDeltaAggregator({
|
|
162
|
+
flushDelta: async ({ text, sessionId, cwd }) => {
|
|
163
|
+
await emitWorkerEvent({
|
|
164
|
+
adapter,
|
|
165
|
+
binding,
|
|
166
|
+
agent: this.name,
|
|
167
|
+
sessionId: sessionId || meta.sessionId || binding.id,
|
|
168
|
+
cwd: cwd || agentHome,
|
|
169
|
+
replyToMessageId: inbound.messageId || null,
|
|
170
|
+
event: {
|
|
171
|
+
hook_event_name: 'worker.message.delta',
|
|
172
|
+
worker_event_name: 'worker.message.delta',
|
|
173
|
+
delta: text,
|
|
174
|
+
},
|
|
175
|
+
logger: ctx.logger,
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
});
|
|
168
179
|
|
|
169
180
|
try {
|
|
170
181
|
const opencodePath = requireOpenCodePath(runtimeOpenCodePath);
|
|
171
182
|
const opencodeVersion = getOpenCodeRuntimeHealth(opencodePath).version || meta.opencodeVersion || null;
|
|
172
183
|
const agentEnv = buildAgentRuntimeEnv({
|
|
173
184
|
agentId: binding.id,
|
|
174
|
-
sessionId:
|
|
185
|
+
sessionId: meta.sessionId,
|
|
175
186
|
hostId: binding.runtime_host_id,
|
|
176
|
-
conversationId: inbound.conversationId,
|
|
177
|
-
messageId: inbound.messageId,
|
|
178
|
-
target: inbound.envelopeTarget,
|
|
179
187
|
});
|
|
180
|
-
const
|
|
181
|
-
const protocolOverlayKey = getGoalTaskProtocolOverlayKey({ agentId: binding.id, inbound });
|
|
182
|
-
const standingPromptSeenKey = targetSessionId
|
|
183
|
-
? `${binding.id}|${targetSessionId}|${protocolOverlayKey}`
|
|
184
|
-
: null;
|
|
185
|
-
const forceStandingPrompt = Boolean(standingPromptSeenKey && !standingPromptSeen.has(standingPromptSeenKey));
|
|
188
|
+
const runtimeBaseInstructions = buildRuntimeBaseInstructions({ agentId: binding.id });
|
|
186
189
|
const result = shouldStreamRuntime(this.name, this)
|
|
187
|
-
? await this.runTurnStream({ sessionId:
|
|
188
|
-
|
|
189
|
-
forceStandingPrompt,
|
|
190
|
+
? await this.runTurnStream({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, opencodePath, agentEnv }, message, {
|
|
191
|
+
runtimeBaseInstructions,
|
|
190
192
|
files,
|
|
191
193
|
onEvent: async (event) => {
|
|
192
194
|
if (event?.type === 'turn.started') {
|
|
193
|
-
|
|
195
|
+
await emitWorkerEvent({
|
|
194
196
|
adapter,
|
|
195
197
|
binding,
|
|
196
198
|
agent: this.name,
|
|
197
|
-
sessionId: event.sessionId ||
|
|
199
|
+
sessionId: event.sessionId || meta.sessionId || binding.id,
|
|
198
200
|
cwd: agentHome,
|
|
199
201
|
replyToMessageId: inbound.messageId || null,
|
|
200
202
|
event: {
|
|
@@ -203,30 +205,31 @@ export const openCodeRuntime = {
|
|
|
203
205
|
},
|
|
204
206
|
logger: ctx.logger,
|
|
205
207
|
});
|
|
208
|
+
} else if (event?.type === 'message.delta' && event.text) {
|
|
209
|
+
deltaAggregator.push(event.text, {
|
|
210
|
+
sessionId: event.sessionId || meta.sessionId || binding.id,
|
|
211
|
+
cwd: agentHome,
|
|
212
|
+
});
|
|
206
213
|
}
|
|
207
214
|
},
|
|
208
215
|
})
|
|
209
|
-
: await this.runTurn({ sessionId:
|
|
216
|
+
: await this.runTurn({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, opencodePath, agentEnv }, message, { files, runtimeBaseInstructions });
|
|
210
217
|
|
|
211
|
-
|
|
212
|
-
if (observedSessionId) {
|
|
213
|
-
standingPromptSeen.add(`${binding.id}|${observedSessionId}|${protocolOverlayKey}`);
|
|
214
|
-
}
|
|
218
|
+
await deltaAggregator.flush();
|
|
215
219
|
|
|
216
220
|
const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
|
|
217
|
-
|
|
218
|
-
sessionId: result?.sessionId,
|
|
219
|
-
path: result?.path,
|
|
220
|
-
}),
|
|
221
|
+
sessionId: result?.sessionId || meta.sessionId,
|
|
221
222
|
runtimePath: opencodePath,
|
|
222
223
|
opencodePath,
|
|
223
224
|
opencodeVersion,
|
|
225
|
+
rotatePending: false,
|
|
226
|
+
lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
|
|
224
227
|
}, { status: 'connected' });
|
|
225
|
-
|
|
228
|
+
await emitWorkerEvent({
|
|
226
229
|
adapter,
|
|
227
230
|
binding: nextBinding,
|
|
228
231
|
agent: this.name,
|
|
229
|
-
sessionId: result?.sessionId ||
|
|
232
|
+
sessionId: result?.sessionId || meta.sessionId || binding.id,
|
|
230
233
|
cwd: result?.cwd || agentHome,
|
|
231
234
|
replyToMessageId: inbound.messageId || null,
|
|
232
235
|
event: {
|
|
@@ -237,11 +240,12 @@ export const openCodeRuntime = {
|
|
|
237
240
|
});
|
|
238
241
|
return true;
|
|
239
242
|
} catch (err) {
|
|
240
|
-
|
|
243
|
+
await deltaAggregator.flush().catch(() => {});
|
|
244
|
+
await emitWorkerEvent({
|
|
241
245
|
adapter,
|
|
242
246
|
binding,
|
|
243
247
|
agent: this.name,
|
|
244
|
-
sessionId:
|
|
248
|
+
sessionId: meta.sessionId || binding.id,
|
|
245
249
|
cwd: agentHome,
|
|
246
250
|
replyToMessageId: inbound.messageId || null,
|
|
247
251
|
event: {
|
|
@@ -267,6 +271,24 @@ export const openCodeRuntime = {
|
|
|
267
271
|
}
|
|
268
272
|
},
|
|
269
273
|
|
|
274
|
+
async reconcileAfterRestart(binding, ctx) {
|
|
275
|
+
const meta = binding.runtimeMeta || {};
|
|
276
|
+
await emitWorkerEvent({
|
|
277
|
+
adapter: ctx.adapter,
|
|
278
|
+
binding,
|
|
279
|
+
agent: this.name,
|
|
280
|
+
sessionId: meta.sessionId || binding.id,
|
|
281
|
+
cwd: ensureAgentHome(binding.id) || '',
|
|
282
|
+
event: {
|
|
283
|
+
hook_event_name: 'Stop',
|
|
284
|
+
worker_event_name: 'worker.turn.complete',
|
|
285
|
+
reason: 'connector.restart.reconcile',
|
|
286
|
+
},
|
|
287
|
+
logger: ctx.logger,
|
|
288
|
+
});
|
|
289
|
+
return 1;
|
|
290
|
+
},
|
|
291
|
+
|
|
270
292
|
dataDir: OPENCODE_DATA_DIR,
|
|
271
293
|
maxAgeMs: OPENCODE_MAX_AGE_MS,
|
|
272
294
|
};
|
|
@@ -193,9 +193,10 @@ function buildOpenCodeRunArgs({ sessionId, message, files = [] }) {
|
|
|
193
193
|
/**
|
|
194
194
|
* Spawn `opencode run` and resolve with the final result.
|
|
195
195
|
*
|
|
196
|
-
* If `onEvent` is provided it is invoked for
|
|
197
|
-
* `{ type: 'turn.started', sessionId }
|
|
198
|
-
*
|
|
196
|
+
* If `onEvent` is provided it is invoked for each parsed event during
|
|
197
|
+
* the turn — `{ type: 'turn.started', sessionId }`,
|
|
198
|
+
* `{ type: 'message.delta', sessionId, text }`, etc. — so callers can
|
|
199
|
+
* forward typing indicators / partial text to the chat client.
|
|
199
200
|
*
|
|
200
201
|
* Resolves with `{ sessionId, cwd, text, durationMs }` on success.
|
|
201
202
|
* Rejects with an Error whose `.info` carries `{ ok: false, kind, ... }`
|
|
@@ -208,17 +209,16 @@ export function runOpenCodePrompt({
|
|
|
208
209
|
files = [],
|
|
209
210
|
opencodePath = null,
|
|
210
211
|
agentEnv = null,
|
|
211
|
-
|
|
212
|
-
forceStandingPrompt = false,
|
|
212
|
+
runtimeBaseInstructions = null,
|
|
213
213
|
timeoutMs = Number(process.env.OPENCODE_RUN_TIMEOUT_MS || DEFAULT_OPENCODE_RUN_TIMEOUT_MS),
|
|
214
214
|
onEvent,
|
|
215
215
|
}) {
|
|
216
216
|
// opencode has no documented `--system` flag, so we prepend the
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
const finalMessage =
|
|
221
|
-
? `${
|
|
217
|
+
// runtime base instructions to the first-turn message body. Resumed sessions
|
|
218
|
+
// (sessionId set) skip injection because the model already saw it
|
|
219
|
+
// on the originating turn.
|
|
220
|
+
const finalMessage = runtimeBaseInstructions && !sessionId
|
|
221
|
+
? `${runtimeBaseInstructions}\n\n---\n\n${message}`
|
|
222
222
|
: message;
|
|
223
223
|
return new Promise((resolve, reject) => {
|
|
224
224
|
const startedAt = Date.now();
|
|
@@ -245,19 +245,18 @@ export function runOpenCodePrompt({
|
|
|
245
245
|
let lastError = null;
|
|
246
246
|
let settled = false;
|
|
247
247
|
let turnStartedEmitted = false;
|
|
248
|
+
let eventChain = Promise.resolve();
|
|
248
249
|
|
|
249
250
|
const emit = (event) => {
|
|
250
251
|
if (typeof onEvent !== 'function') return;
|
|
251
|
-
|
|
252
|
-
.then(() => onEvent(event))
|
|
253
|
-
.catch(() => {});
|
|
252
|
+
eventChain = eventChain.then(() => onEvent(event)).catch(() => {});
|
|
254
253
|
};
|
|
255
254
|
|
|
256
255
|
const settle = (fn, value) => {
|
|
257
256
|
if (settled) return;
|
|
258
257
|
settled = true;
|
|
259
258
|
if (timeout) clearTimeout(timeout);
|
|
260
|
-
fn(value);
|
|
259
|
+
eventChain.catch(() => {}).finally(() => fn(value));
|
|
261
260
|
};
|
|
262
261
|
|
|
263
262
|
const handleEvent = (event) => {
|
|
@@ -273,6 +272,7 @@ export function runOpenCodePrompt({
|
|
|
273
272
|
const delta = extractEventText(event);
|
|
274
273
|
if (delta) {
|
|
275
274
|
finalText += delta;
|
|
275
|
+
emit({ type: 'message.delta', sessionId: activeSessionId, text: delta });
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
278
|
|