ticlawk 0.1.16-dev.9 → 0.1.17-dev.1
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 +17 -3
- package/bin/ticlawk.mjs +255 -21
- package/package.json +1 -1
- package/src/adapters/ticlawk/api.mjs +350 -50
- package/src/adapters/ticlawk/credentials.mjs +41 -1
- package/src/adapters/ticlawk/index.mjs +248 -130
- package/src/adapters/ticlawk/wake-client.mjs +1 -1
- package/src/cli/agent-commands.mjs +715 -18
- package/src/core/agent-cli-handlers.mjs +556 -18
- package/src/core/agent-home.mjs +81 -1
- package/src/core/argv.mjs +11 -1
- package/src/core/events/worker-events.mjs +32 -36
- package/src/core/http.mjs +152 -0
- package/src/core/runtime-contract.mjs +0 -1
- package/src/core/runtime-env.mjs +7 -0
- package/src/core/runtime-support.mjs +130 -78
- package/src/runtimes/_shared/agent-handbook.mjs +45 -0
- package/src/runtimes/_shared/brand.mjs +2 -0
- package/src/runtimes/_shared/goal-step-prompt.mjs +98 -0
- package/src/runtimes/_shared/goal-task-protocol.mjs +50 -0
- package/src/runtimes/_shared/handbook/BASICS.md +27 -0
- package/src/runtimes/_shared/handbook/COLLABORATION.md +37 -0
- package/src/runtimes/_shared/handbook/COMMUNICATION.md +55 -0
- package/src/runtimes/_shared/handbook/DM_SCOPE.md +13 -0
- package/src/runtimes/_shared/handbook/GOAL_AUTHORITY.md +47 -0
- package/src/runtimes/_shared/handbook/GOAL_TASK_CORE.md +43 -0
- package/src/runtimes/_shared/handbook/GROUP_ADMIN_SCOPE.md +21 -0
- package/src/runtimes/_shared/handbook/GROUP_MEMBER_SCOPE.md +15 -0
- package/src/runtimes/_shared/handbook/SURFACES.md +41 -0
- package/src/runtimes/_shared/handbook/TASK_WORKER.md +14 -0
- package/src/runtimes/_shared/standing-prompt.mjs +124 -279
- package/src/runtimes/_shared/wake-prompt.mjs +268 -0
- package/src/runtimes/claude-code/index.mjs +19 -46
- package/src/runtimes/claude-code/session.mjs +2 -7
- package/src/runtimes/codex/index.mjs +115 -63
- package/src/runtimes/codex/session.mjs +2 -12
- package/src/runtimes/openclaw/index.mjs +11 -24
- package/src/runtimes/opencode/index.mjs +38 -60
- package/src/runtimes/opencode/session.mjs +12 -12
- package/src/runtimes/pi/index.mjs +38 -60
- package/src/runtimes/pi/session.mjs +9 -6
- package/ticlawk.mjs +0 -30
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
* and discover local sessions.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
import { basename, join } from 'node:path';
|
|
9
12
|
import {
|
|
10
13
|
createCodexSession,
|
|
11
14
|
runCodexPrompt,
|
|
@@ -18,18 +21,97 @@ import {
|
|
|
18
21
|
requireCodexPath,
|
|
19
22
|
} from './session.mjs';
|
|
20
23
|
import { buildCodexInputFromInbound } from '../../core/media/inbound.mjs';
|
|
21
|
-
import {
|
|
24
|
+
import { emitWorkerEventBestEffort } from '../../core/events/worker-events.mjs';
|
|
22
25
|
import {
|
|
23
26
|
shouldStreamRuntime,
|
|
24
|
-
createDeltaAggregator,
|
|
25
27
|
reportSubprocessFailure,
|
|
26
28
|
terminalRuntimeFailure,
|
|
27
29
|
updateBindingRuntimeMeta,
|
|
28
30
|
isRuntimeGatewayFailure,
|
|
31
|
+
resolveRuntimeSessionScope,
|
|
32
|
+
buildRuntimeSessionMetaPatch,
|
|
29
33
|
} from '../../core/runtime-support.mjs';
|
|
30
34
|
import { buildAgentRuntimeEnv } from '../../core/runtime-env.mjs';
|
|
31
35
|
import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
|
|
32
36
|
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
|
+
}
|
|
33
115
|
|
|
34
116
|
export const codexRuntime = {
|
|
35
117
|
name: 'codex',
|
|
@@ -140,46 +222,42 @@ export const codexRuntime = {
|
|
|
140
222
|
? (codexInput.find((item) => item?.type === 'text')?.text || inbound.text || '(image attached)')
|
|
141
223
|
: inbound.text;
|
|
142
224
|
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
|
|
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
|
-
});
|
|
225
|
+
const sessionScope = resolveRuntimeSessionScope(meta, inbound);
|
|
226
|
+
const shouldRotate = sessionScope.shouldRotate;
|
|
227
|
+
const targetSessionId = shouldRotate ? null : sessionScope.sessionId;
|
|
163
228
|
try {
|
|
164
229
|
const runtimeCodexPath = requireCodexPath(meta.codexPath || meta.runtimePath);
|
|
165
230
|
const runtimeCodexVersion = getCodexRuntimeHealth(runtimeCodexPath).version || meta.codexVersion || null;
|
|
166
231
|
const agentEnv = buildAgentRuntimeEnv({
|
|
167
232
|
agentId: binding.id,
|
|
168
|
-
sessionId:
|
|
233
|
+
sessionId: targetSessionId,
|
|
169
234
|
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,
|
|
170
249
|
});
|
|
171
|
-
const developerInstructions = buildStandingPrompt({ agentId: binding.id });
|
|
172
250
|
const result = (shouldRotate || inbound.action === 'image' || shouldStreamRuntime(this.name, this))
|
|
173
|
-
? await this.runTurnStream({ sessionId:
|
|
251
|
+
? await this.runTurnStream({ sessionId: targetSessionId, cwd: agentHome, codexPath: runtimeCodexPath, agentEnv }, message, {
|
|
174
252
|
input: codexInput,
|
|
175
253
|
developerInstructions,
|
|
176
254
|
onEvent: async (event) => {
|
|
177
255
|
if (event?.type === 'turn.started') {
|
|
178
|
-
|
|
256
|
+
emitWorkerEventBestEffort({
|
|
179
257
|
adapter,
|
|
180
258
|
binding,
|
|
181
259
|
agent: this.name,
|
|
182
|
-
sessionId: event.sessionId ||
|
|
260
|
+
sessionId: event.sessionId || targetSessionId || binding.id,
|
|
183
261
|
turnId: event.turnId || null,
|
|
184
262
|
cwd: agentHome,
|
|
185
263
|
replyToMessageId: inbound.messageId || null,
|
|
@@ -189,34 +267,27 @@ export const codexRuntime = {
|
|
|
189
267
|
},
|
|
190
268
|
logger: ctx.logger,
|
|
191
269
|
});
|
|
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
|
-
});
|
|
198
270
|
}
|
|
199
271
|
},
|
|
200
272
|
})
|
|
201
|
-
: await this.runTurn({ sessionId:
|
|
273
|
+
: await this.runTurn({ sessionId: targetSessionId, cwd: agentHome, codexPath: runtimeCodexPath, agentEnv }, message, {
|
|
202
274
|
input: codexInput,
|
|
203
275
|
});
|
|
204
276
|
|
|
205
|
-
await deltaAggregator.flush();
|
|
206
277
|
const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
|
|
207
|
-
|
|
208
|
-
|
|
278
|
+
...buildRuntimeSessionMetaPatch(meta, sessionScope, {
|
|
279
|
+
sessionId: result?.sessionId,
|
|
280
|
+
path: result?.path,
|
|
281
|
+
}),
|
|
209
282
|
runtimePath: runtimeCodexPath,
|
|
210
283
|
codexPath: runtimeCodexPath,
|
|
211
284
|
codexVersion: runtimeCodexVersion,
|
|
212
|
-
rotatePending: false,
|
|
213
|
-
lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
|
|
214
285
|
}, { status: 'connected' });
|
|
215
|
-
|
|
286
|
+
emitWorkerEventBestEffort({
|
|
216
287
|
adapter,
|
|
217
288
|
binding: nextBinding,
|
|
218
289
|
agent: this.name,
|
|
219
|
-
sessionId: result?.sessionId ||
|
|
290
|
+
sessionId: result?.sessionId || targetSessionId || binding.id,
|
|
220
291
|
turnId: result?.turnId || null,
|
|
221
292
|
cwd: result?.cwd || agentHome,
|
|
222
293
|
replyToMessageId: inbound.messageId || null,
|
|
@@ -228,7 +299,6 @@ export const codexRuntime = {
|
|
|
228
299
|
});
|
|
229
300
|
return true;
|
|
230
301
|
} catch (err) {
|
|
231
|
-
await deltaAggregator.flush().catch(() => {});
|
|
232
302
|
const failureInfo = err?.info || {
|
|
233
303
|
ok: false,
|
|
234
304
|
kind: 'exit-error',
|
|
@@ -243,11 +313,11 @@ export const codexRuntime = {
|
|
|
243
313
|
lastGatewayFailureReason: failureInfo.errorMessage || err?.message || 'Codex app-server error',
|
|
244
314
|
}, { status: 'degraded' }).catch(() => binding);
|
|
245
315
|
}
|
|
246
|
-
|
|
316
|
+
emitWorkerEventBestEffort({
|
|
247
317
|
adapter,
|
|
248
318
|
binding: failureBinding,
|
|
249
319
|
agent: this.name,
|
|
250
|
-
sessionId:
|
|
320
|
+
sessionId: targetSessionId || binding.id,
|
|
251
321
|
turnId: failureInfo?.turnId || null,
|
|
252
322
|
cwd: agentHome,
|
|
253
323
|
replyToMessageId: inbound.messageId || null,
|
|
@@ -269,24 +339,6 @@ export const codexRuntime = {
|
|
|
269
339
|
}
|
|
270
340
|
},
|
|
271
341
|
|
|
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
|
-
|
|
290
342
|
sessionsDir: CODEX_SESSIONS_DIR,
|
|
291
343
|
maxAgeMs: CODEX_MAX_AGE_MS,
|
|
292
344
|
};
|
|
@@ -368,14 +368,12 @@ export function streamCodexPrompt({
|
|
|
368
368
|
let finalText = '';
|
|
369
369
|
let threadPath = null;
|
|
370
370
|
const ignoredChildEventsLogged = new Set();
|
|
371
|
-
let eventChain = Promise.resolve();
|
|
372
371
|
|
|
373
372
|
const emit = (event) => {
|
|
374
373
|
if (typeof onEvent !== 'function') return;
|
|
375
|
-
|
|
374
|
+
void Promise.resolve()
|
|
376
375
|
.then(() => onEvent(event))
|
|
377
376
|
.catch(() => {});
|
|
378
|
-
return eventChain;
|
|
379
377
|
};
|
|
380
378
|
|
|
381
379
|
const settle = (fn, value) => {
|
|
@@ -387,9 +385,7 @@ export function streamCodexPrompt({
|
|
|
387
385
|
}
|
|
388
386
|
pending.clear();
|
|
389
387
|
try { child.kill('SIGTERM'); } catch {}
|
|
390
|
-
|
|
391
|
-
.catch(() => {})
|
|
392
|
-
.finally(() => fn(value));
|
|
388
|
+
fn(value);
|
|
393
389
|
};
|
|
394
390
|
|
|
395
391
|
const send = (method, params = {}) => {
|
|
@@ -451,12 +447,6 @@ export function streamCodexPrompt({
|
|
|
451
447
|
const delta = params?.delta;
|
|
452
448
|
if (typeof delta === 'string' && delta && isRootContext(params)) {
|
|
453
449
|
finalText += delta;
|
|
454
|
-
emit({
|
|
455
|
-
type: 'message.delta',
|
|
456
|
-
sessionId: rootThreadId,
|
|
457
|
-
turnId: rootTurnId,
|
|
458
|
-
text: delta,
|
|
459
|
-
});
|
|
460
450
|
} else if (typeof delta === 'string' && delta) {
|
|
461
451
|
logIgnoredChildEvent('codex.non-root-agent-delta', params);
|
|
462
452
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { reportSubprocessFailure, terminalRuntimeFailure } from '../../core/runtime-support.mjs';
|
|
2
|
+
import { getGoalTaskProtocolOverlayKey } from '../_shared/goal-task-protocol.mjs';
|
|
2
3
|
import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
|
|
3
4
|
import { GATEWAY_HOST, GATEWAY_PORT } from './identity.mjs';
|
|
4
5
|
|
|
@@ -24,10 +25,11 @@ async function probeOpenClawGatewayHealth() {
|
|
|
24
25
|
import { buildOpenClawSessionKey, normalizeOpenClawAgentId } from './target.mjs';
|
|
25
26
|
import { askGateway, isGatewayReady, registerOpenClawChannel } from './gateway.mjs';
|
|
26
27
|
|
|
27
|
-
// Tracks which (agentId, sessionKey
|
|
28
|
-
// prompt this process lifetime. OpenClaw's
|
|
29
|
-
// out of process, so we err on the side of
|
|
30
|
-
//
|
|
28
|
+
// Tracks which (agentId, sessionKey, protocol overlay) combinations
|
|
29
|
+
// already saw the standing prompt this process lifetime. OpenClaw's
|
|
30
|
+
// gateway holds session state out of process, so we err on the side of
|
|
31
|
+
// "inject on first observed overlay after daemon restart"; the gateway
|
|
32
|
+
// dedupes redundant context.
|
|
31
33
|
const standingPromptSeen = new Set();
|
|
32
34
|
import { addInFlight, recoverInFlightEntries, removeInFlight } from './inflight.mjs';
|
|
33
35
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
@@ -159,13 +161,13 @@ export const openClawRuntime = {
|
|
|
159
161
|
const sessionId = String(meta.sessionKey || buildOpenClawSessionKey(agentId)).trim();
|
|
160
162
|
|
|
161
163
|
// Inject the standing prompt on the first observed turn after a
|
|
162
|
-
// daemon start for this (agent, session)
|
|
163
|
-
// separate "system prompt" parameter on the gateway, so we
|
|
164
|
-
//
|
|
165
|
-
const standingKey = `${agentId}|${sessionId}`;
|
|
164
|
+
// daemon start for this (agent, session, protocol overlay). OpenClaw
|
|
165
|
+
// has no separate "system prompt" parameter on the gateway, so we
|
|
166
|
+
// prepend through the prompt body.
|
|
167
|
+
const standingKey = `${agentId}|${sessionId}|${getGoalTaskProtocolOverlayKey({ agentId: binding.id, inbound })}`;
|
|
166
168
|
let prompt = rawPrompt;
|
|
167
169
|
if (!standingPromptSeen.has(standingKey)) {
|
|
168
|
-
const standing = buildStandingPrompt({ agentId: binding.id });
|
|
170
|
+
const standing = buildStandingPrompt({ agentId: binding.id, inbound });
|
|
169
171
|
prompt = `${standing}\n\n---\n\n${rawPrompt}`;
|
|
170
172
|
standingPromptSeen.add(standingKey);
|
|
171
173
|
}
|
|
@@ -236,21 +238,6 @@ export const openClawRuntime = {
|
|
|
236
238
|
return entries.length;
|
|
237
239
|
},
|
|
238
240
|
|
|
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
|
-
},
|
|
254
241
|
};
|
|
255
242
|
|
|
256
243
|
export default openClawRuntime;
|
|
@@ -10,6 +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 { getGoalTaskProtocolOverlayKey } from '../_shared/goal-task-protocol.mjs';
|
|
13
14
|
import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
|
|
14
15
|
import { ensureAgentHome } from '../../core/agent-home.mjs';
|
|
15
16
|
import {
|
|
@@ -24,15 +25,18 @@ import {
|
|
|
24
25
|
requireOpenCodePath,
|
|
25
26
|
} from './session.mjs';
|
|
26
27
|
import { buildOpenCodeInputFromInbound } from '../../core/media/inbound.mjs';
|
|
27
|
-
import {
|
|
28
|
+
import { emitWorkerEventBestEffort } from '../../core/events/worker-events.mjs';
|
|
28
29
|
import {
|
|
29
30
|
shouldStreamRuntime,
|
|
30
|
-
createDeltaAggregator,
|
|
31
31
|
reportSubprocessFailure,
|
|
32
32
|
terminalRuntimeFailure,
|
|
33
33
|
updateBindingRuntimeMeta,
|
|
34
|
+
resolveRuntimeSessionScope,
|
|
35
|
+
buildRuntimeSessionMetaPatch,
|
|
34
36
|
} from '../../core/runtime-support.mjs';
|
|
35
37
|
|
|
38
|
+
const standingPromptSeen = new Set();
|
|
39
|
+
|
|
36
40
|
export const openCodeRuntime = {
|
|
37
41
|
name: 'opencode',
|
|
38
42
|
|
|
@@ -48,6 +52,7 @@ export const openCodeRuntime = {
|
|
|
48
52
|
opencodePath,
|
|
49
53
|
agentEnv,
|
|
50
54
|
standingPrompt: opts.standingPrompt || null,
|
|
55
|
+
forceStandingPrompt: Boolean(opts.forceStandingPrompt),
|
|
51
56
|
files: opts.files,
|
|
52
57
|
timeoutMs: opts.timeoutMs,
|
|
53
58
|
});
|
|
@@ -61,6 +66,7 @@ export const openCodeRuntime = {
|
|
|
61
66
|
opencodePath,
|
|
62
67
|
agentEnv,
|
|
63
68
|
standingPrompt: opts.standingPrompt || null,
|
|
69
|
+
forceStandingPrompt: Boolean(opts.forceStandingPrompt),
|
|
64
70
|
files: opts.files,
|
|
65
71
|
timeoutMs: opts.timeoutMs,
|
|
66
72
|
onEvent: opts.onEvent,
|
|
@@ -156,47 +162,39 @@ export const openCodeRuntime = {
|
|
|
156
162
|
// instruction so it has something to anchor on.
|
|
157
163
|
message = captionText || 'Please analyze the attached image(s).';
|
|
158
164
|
}
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
const
|
|
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
|
-
});
|
|
165
|
+
const sessionScope = resolveRuntimeSessionScope(meta, inbound);
|
|
166
|
+
const shouldRotate = sessionScope.shouldRotate;
|
|
167
|
+
const targetSessionId = shouldRotate ? null : sessionScope.sessionId;
|
|
179
168
|
|
|
180
169
|
try {
|
|
181
170
|
const opencodePath = requireOpenCodePath(runtimeOpenCodePath);
|
|
182
171
|
const opencodeVersion = getOpenCodeRuntimeHealth(opencodePath).version || meta.opencodeVersion || null;
|
|
183
172
|
const agentEnv = buildAgentRuntimeEnv({
|
|
184
173
|
agentId: binding.id,
|
|
185
|
-
sessionId:
|
|
174
|
+
sessionId: targetSessionId,
|
|
186
175
|
hostId: binding.runtime_host_id,
|
|
176
|
+
conversationId: inbound.conversationId,
|
|
177
|
+
messageId: inbound.messageId,
|
|
178
|
+
target: inbound.envelopeTarget,
|
|
187
179
|
});
|
|
188
|
-
const standingPrompt = buildStandingPrompt({ agentId: binding.id });
|
|
180
|
+
const standingPrompt = buildStandingPrompt({ agentId: binding.id, inbound });
|
|
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));
|
|
189
186
|
const result = shouldStreamRuntime(this.name, this)
|
|
190
|
-
? await this.runTurnStream({ sessionId:
|
|
187
|
+
? await this.runTurnStream({ sessionId: targetSessionId, cwd: agentHome, opencodePath, agentEnv }, message, {
|
|
191
188
|
standingPrompt,
|
|
189
|
+
forceStandingPrompt,
|
|
192
190
|
files,
|
|
193
191
|
onEvent: async (event) => {
|
|
194
192
|
if (event?.type === 'turn.started') {
|
|
195
|
-
|
|
193
|
+
emitWorkerEventBestEffort({
|
|
196
194
|
adapter,
|
|
197
195
|
binding,
|
|
198
196
|
agent: this.name,
|
|
199
|
-
sessionId: event.sessionId ||
|
|
197
|
+
sessionId: event.sessionId || targetSessionId || binding.id,
|
|
200
198
|
cwd: agentHome,
|
|
201
199
|
replyToMessageId: inbound.messageId || null,
|
|
202
200
|
event: {
|
|
@@ -205,31 +203,30 @@ export const openCodeRuntime = {
|
|
|
205
203
|
},
|
|
206
204
|
logger: ctx.logger,
|
|
207
205
|
});
|
|
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
|
-
});
|
|
213
206
|
}
|
|
214
207
|
},
|
|
215
208
|
})
|
|
216
|
-
: await this.runTurn({ sessionId:
|
|
209
|
+
: await this.runTurn({ sessionId: targetSessionId, cwd: agentHome, opencodePath, agentEnv }, message, { files, standingPrompt, forceStandingPrompt });
|
|
217
210
|
|
|
218
|
-
|
|
211
|
+
const observedSessionId = result?.sessionId || targetSessionId;
|
|
212
|
+
if (observedSessionId) {
|
|
213
|
+
standingPromptSeen.add(`${binding.id}|${observedSessionId}|${protocolOverlayKey}`);
|
|
214
|
+
}
|
|
219
215
|
|
|
220
216
|
const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
|
|
221
|
-
|
|
217
|
+
...buildRuntimeSessionMetaPatch(meta, sessionScope, {
|
|
218
|
+
sessionId: result?.sessionId,
|
|
219
|
+
path: result?.path,
|
|
220
|
+
}),
|
|
222
221
|
runtimePath: opencodePath,
|
|
223
222
|
opencodePath,
|
|
224
223
|
opencodeVersion,
|
|
225
|
-
rotatePending: false,
|
|
226
|
-
lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
|
|
227
224
|
}, { status: 'connected' });
|
|
228
|
-
|
|
225
|
+
emitWorkerEventBestEffort({
|
|
229
226
|
adapter,
|
|
230
227
|
binding: nextBinding,
|
|
231
228
|
agent: this.name,
|
|
232
|
-
sessionId: result?.sessionId ||
|
|
229
|
+
sessionId: result?.sessionId || targetSessionId || binding.id,
|
|
233
230
|
cwd: result?.cwd || agentHome,
|
|
234
231
|
replyToMessageId: inbound.messageId || null,
|
|
235
232
|
event: {
|
|
@@ -240,12 +237,11 @@ export const openCodeRuntime = {
|
|
|
240
237
|
});
|
|
241
238
|
return true;
|
|
242
239
|
} catch (err) {
|
|
243
|
-
|
|
244
|
-
await emitWorkerEvent({
|
|
240
|
+
emitWorkerEventBestEffort({
|
|
245
241
|
adapter,
|
|
246
242
|
binding,
|
|
247
243
|
agent: this.name,
|
|
248
|
-
sessionId:
|
|
244
|
+
sessionId: targetSessionId || binding.id,
|
|
249
245
|
cwd: agentHome,
|
|
250
246
|
replyToMessageId: inbound.messageId || null,
|
|
251
247
|
event: {
|
|
@@ -271,24 +267,6 @@ export const openCodeRuntime = {
|
|
|
271
267
|
}
|
|
272
268
|
},
|
|
273
269
|
|
|
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
|
-
|
|
292
270
|
dataDir: OPENCODE_DATA_DIR,
|
|
293
271
|
maxAgeMs: OPENCODE_MAX_AGE_MS,
|
|
294
272
|
};
|
|
@@ -193,10 +193,9 @@ 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
|
-
*
|
|
198
|
-
*
|
|
199
|
-
* forward typing indicators / partial text to the chat client.
|
|
196
|
+
* If `onEvent` is provided it is invoked for lifecycle events such as
|
|
197
|
+
* `{ type: 'turn.started', sessionId }`. Token deltas are kept local to
|
|
198
|
+
* build the final text and are not emitted to the caller.
|
|
200
199
|
*
|
|
201
200
|
* Resolves with `{ sessionId, cwd, text, durationMs }` on success.
|
|
202
201
|
* Rejects with an Error whose `.info` carries `{ ok: false, kind, ... }`
|
|
@@ -210,14 +209,15 @@ export function runOpenCodePrompt({
|
|
|
210
209
|
opencodePath = null,
|
|
211
210
|
agentEnv = null,
|
|
212
211
|
standingPrompt = null,
|
|
212
|
+
forceStandingPrompt = false,
|
|
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
|
-
// standing prompt to the first-turn message body.
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
const finalMessage = standingPrompt && !sessionId
|
|
217
|
+
// standing prompt to the first-turn message body. Callers may force a
|
|
218
|
+
// resumed-session injection when the selected protocol overlay changes
|
|
219
|
+
// or this daemon process has not yet observed the session.
|
|
220
|
+
const finalMessage = standingPrompt && (!sessionId || forceStandingPrompt)
|
|
221
221
|
? `${standingPrompt}\n\n---\n\n${message}`
|
|
222
222
|
: message;
|
|
223
223
|
return new Promise((resolve, reject) => {
|
|
@@ -245,18 +245,19 @@ export function runOpenCodePrompt({
|
|
|
245
245
|
let lastError = null;
|
|
246
246
|
let settled = false;
|
|
247
247
|
let turnStartedEmitted = false;
|
|
248
|
-
let eventChain = Promise.resolve();
|
|
249
248
|
|
|
250
249
|
const emit = (event) => {
|
|
251
250
|
if (typeof onEvent !== 'function') return;
|
|
252
|
-
|
|
251
|
+
void Promise.resolve()
|
|
252
|
+
.then(() => onEvent(event))
|
|
253
|
+
.catch(() => {});
|
|
253
254
|
};
|
|
254
255
|
|
|
255
256
|
const settle = (fn, value) => {
|
|
256
257
|
if (settled) return;
|
|
257
258
|
settled = true;
|
|
258
259
|
if (timeout) clearTimeout(timeout);
|
|
259
|
-
|
|
260
|
+
fn(value);
|
|
260
261
|
};
|
|
261
262
|
|
|
262
263
|
const handleEvent = (event) => {
|
|
@@ -272,7 +273,6 @@ export function runOpenCodePrompt({
|
|
|
272
273
|
const delta = extractEventText(event);
|
|
273
274
|
if (delta) {
|
|
274
275
|
finalText += delta;
|
|
275
|
-
emit({ type: 'message.delta', sessionId: activeSessionId, text: delta });
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
278
|
|