ticlawk 0.1.16-dev.3 → 0.1.16-dev.31
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 +14 -2
- package/bin/ticlawk.mjs +207 -25
- package/package.json +1 -1
- package/src/adapters/ticlawk/api.mjs +293 -70
- package/src/adapters/ticlawk/credentials.mjs +41 -1
- package/src/adapters/ticlawk/index.mjs +199 -199
- package/src/adapters/ticlawk/wake-client.mjs +1 -1
- package/src/cli/agent-commands.mjs +607 -37
- package/src/core/agent-cli-handlers.mjs +449 -20
- package/src/core/agent-home.mjs +86 -10
- package/src/core/argv.mjs +11 -1
- package/src/core/events/worker-events.mjs +32 -36
- package/src/core/http.mjs +126 -0
- package/src/core/runtime-env.mjs +7 -0
- package/src/core/runtime-support.mjs +108 -107
- package/src/migrate/write-initial-memory.mjs +5 -5
- package/src/runtimes/_shared/agent-handbook.mjs +45 -0
- package/src/runtimes/_shared/brand.mjs +2 -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 +50 -0
- package/src/runtimes/_shared/handbook/DM_SCOPE.md +13 -0
- package/src/runtimes/_shared/handbook/GOAL_AUTHORITY.md +43 -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 +111 -262
- package/src/runtimes/_shared/wake-prompt.mjs +261 -0
- package/src/runtimes/claude-code/index.mjs +34 -127
- package/src/runtimes/claude-code/session.mjs +2 -7
- package/src/runtimes/codex/index.mjs +117 -54
- package/src/runtimes/codex/session.mjs +2 -12
- package/src/runtimes/openclaw/index.mjs +16 -26
- package/src/runtimes/opencode/index.mjs +45 -66
- package/src/runtimes/opencode/session.mjs +12 -12
- package/src/runtimes/pi/index.mjs +42 -60
- package/src/runtimes/pi/session.mjs +9 -6
- package/src/adapters/ticlawk/cards.mjs +0 -149
- package/src/core/media/outbound.mjs +0 -163
|
@@ -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,18 +25,18 @@ import {
|
|
|
24
25
|
requireOpenCodePath,
|
|
25
26
|
} from './session.mjs';
|
|
26
27
|
import { buildOpenCodeInputFromInbound } from '../../core/media/inbound.mjs';
|
|
27
|
-
import {
|
|
28
|
-
import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
|
|
28
|
+
import { emitWorkerEventBestEffort } from '../../core/events/worker-events.mjs';
|
|
29
29
|
import {
|
|
30
30
|
shouldStreamRuntime,
|
|
31
|
-
createDeltaAggregator,
|
|
32
|
-
sendAdapterMessage,
|
|
33
|
-
recordActivity,
|
|
34
31
|
reportSubprocessFailure,
|
|
35
32
|
terminalRuntimeFailure,
|
|
36
33
|
updateBindingRuntimeMeta,
|
|
34
|
+
resolveRuntimeSessionScope,
|
|
35
|
+
buildRuntimeSessionMetaPatch,
|
|
37
36
|
} from '../../core/runtime-support.mjs';
|
|
38
37
|
|
|
38
|
+
const standingPromptSeen = new Set();
|
|
39
|
+
|
|
39
40
|
export const openCodeRuntime = {
|
|
40
41
|
name: 'opencode',
|
|
41
42
|
|
|
@@ -51,6 +52,7 @@ export const openCodeRuntime = {
|
|
|
51
52
|
opencodePath,
|
|
52
53
|
agentEnv,
|
|
53
54
|
standingPrompt: opts.standingPrompt || null,
|
|
55
|
+
forceStandingPrompt: Boolean(opts.forceStandingPrompt),
|
|
54
56
|
files: opts.files,
|
|
55
57
|
timeoutMs: opts.timeoutMs,
|
|
56
58
|
});
|
|
@@ -64,6 +66,7 @@ export const openCodeRuntime = {
|
|
|
64
66
|
opencodePath,
|
|
65
67
|
agentEnv,
|
|
66
68
|
standingPrompt: opts.standingPrompt || null,
|
|
69
|
+
forceStandingPrompt: Boolean(opts.forceStandingPrompt),
|
|
67
70
|
files: opts.files,
|
|
68
71
|
timeoutMs: opts.timeoutMs,
|
|
69
72
|
onEvent: opts.onEvent,
|
|
@@ -146,70 +149,52 @@ export const openCodeRuntime = {
|
|
|
146
149
|
const captionText = (inbound.text || '').trim();
|
|
147
150
|
|
|
148
151
|
if (files.length === 0 && !captionText) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
replyToMessageId: inbound.messageId || null,
|
|
154
|
-
});
|
|
152
|
+
// Image decode failed and no caption to fall back on — we have
|
|
153
|
+
// nothing meaningful to feed the model. Bail without a user
|
|
154
|
+
// notice; this runtime is non-primary and the dead chat-projection
|
|
155
|
+
// path that used to surface such notices is gone.
|
|
155
156
|
return true;
|
|
156
157
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
// Downloads all failed; tell the user we're proceeding with the caption alone.
|
|
160
|
-
await sendAdapterMessage(adapter, binding, {
|
|
161
|
-
type: 'assistant',
|
|
162
|
-
text: '⚠️ Could not access the attached image data; acting on the caption text only.',
|
|
163
|
-
media: [],
|
|
164
|
-
replyToMessageId: inbound.messageId || null,
|
|
165
|
-
});
|
|
166
|
-
}
|
|
158
|
+
// If files.length === 0 && captionText, fall through with the
|
|
159
|
+
// caption-only message below — no inline user notice.
|
|
167
160
|
|
|
168
161
|
// If user sent images with no caption, give the model a minimal
|
|
169
162
|
// instruction so it has something to anchor on.
|
|
170
163
|
message = captionText || 'Please analyze the attached image(s).';
|
|
171
164
|
}
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
flushDelta: async ({ text, sessionId, cwd }) => {
|
|
176
|
-
await emitWorkerEvent({
|
|
177
|
-
adapter,
|
|
178
|
-
binding,
|
|
179
|
-
agent: this.name,
|
|
180
|
-
sessionId: sessionId || meta.sessionId || binding.id,
|
|
181
|
-
cwd: cwd || agentHome,
|
|
182
|
-
replyToMessageId: inbound.messageId || null,
|
|
183
|
-
event: {
|
|
184
|
-
hook_event_name: 'worker.message.delta',
|
|
185
|
-
worker_event_name: 'worker.message.delta',
|
|
186
|
-
delta: text,
|
|
187
|
-
},
|
|
188
|
-
logger: ctx.logger,
|
|
189
|
-
});
|
|
190
|
-
},
|
|
191
|
-
});
|
|
165
|
+
const sessionScope = resolveRuntimeSessionScope(meta, inbound);
|
|
166
|
+
const shouldRotate = sessionScope.shouldRotate;
|
|
167
|
+
const targetSessionId = shouldRotate ? null : sessionScope.sessionId;
|
|
192
168
|
|
|
193
169
|
try {
|
|
194
170
|
const opencodePath = requireOpenCodePath(runtimeOpenCodePath);
|
|
195
171
|
const opencodeVersion = getOpenCodeRuntimeHealth(opencodePath).version || meta.opencodeVersion || null;
|
|
196
172
|
const agentEnv = buildAgentRuntimeEnv({
|
|
197
173
|
agentId: binding.id,
|
|
198
|
-
sessionId:
|
|
174
|
+
sessionId: targetSessionId,
|
|
199
175
|
hostId: binding.runtime_host_id,
|
|
176
|
+
conversationId: inbound.conversationId,
|
|
177
|
+
messageId: inbound.messageId,
|
|
178
|
+
target: inbound.envelopeTarget,
|
|
200
179
|
});
|
|
201
|
-
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));
|
|
202
186
|
const result = shouldStreamRuntime(this.name, this)
|
|
203
|
-
? await this.runTurnStream({ sessionId:
|
|
187
|
+
? await this.runTurnStream({ sessionId: targetSessionId, cwd: agentHome, opencodePath, agentEnv }, message, {
|
|
204
188
|
standingPrompt,
|
|
189
|
+
forceStandingPrompt,
|
|
205
190
|
files,
|
|
206
191
|
onEvent: async (event) => {
|
|
207
192
|
if (event?.type === 'turn.started') {
|
|
208
|
-
|
|
193
|
+
emitWorkerEventBestEffort({
|
|
209
194
|
adapter,
|
|
210
195
|
binding,
|
|
211
196
|
agent: this.name,
|
|
212
|
-
sessionId: event.sessionId ||
|
|
197
|
+
sessionId: event.sessionId || targetSessionId || binding.id,
|
|
213
198
|
cwd: agentHome,
|
|
214
199
|
replyToMessageId: inbound.messageId || null,
|
|
215
200
|
event: {
|
|
@@ -218,35 +203,30 @@ export const openCodeRuntime = {
|
|
|
218
203
|
},
|
|
219
204
|
logger: ctx.logger,
|
|
220
205
|
});
|
|
221
|
-
} else if (event?.type === 'message.delta' && event.text) {
|
|
222
|
-
deltaAggregator.push(event.text, {
|
|
223
|
-
sessionId: event.sessionId || meta.sessionId || binding.id,
|
|
224
|
-
cwd: agentHome,
|
|
225
|
-
});
|
|
226
206
|
}
|
|
227
207
|
},
|
|
228
208
|
})
|
|
229
|
-
: await this.runTurn({ sessionId:
|
|
209
|
+
: await this.runTurn({ sessionId: targetSessionId, cwd: agentHome, opencodePath, agentEnv }, message, { files, standingPrompt, forceStandingPrompt });
|
|
230
210
|
|
|
231
|
-
|
|
211
|
+
const observedSessionId = result?.sessionId || targetSessionId;
|
|
212
|
+
if (observedSessionId) {
|
|
213
|
+
standingPromptSeen.add(`${binding.id}|${observedSessionId}|${protocolOverlayKey}`);
|
|
214
|
+
}
|
|
232
215
|
|
|
233
216
|
const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
|
|
234
|
-
|
|
217
|
+
...buildRuntimeSessionMetaPatch(meta, sessionScope, {
|
|
218
|
+
sessionId: result?.sessionId,
|
|
219
|
+
path: result?.path,
|
|
220
|
+
}),
|
|
235
221
|
runtimePath: opencodePath,
|
|
236
222
|
opencodePath,
|
|
237
223
|
opencodeVersion,
|
|
238
|
-
rotatePending: false,
|
|
239
|
-
lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
|
|
240
224
|
}, { status: 'connected' });
|
|
241
|
-
|
|
242
|
-
...result,
|
|
243
|
-
media: normalizeOutboundMedia(result),
|
|
244
|
-
});
|
|
245
|
-
await emitWorkerEvent({
|
|
225
|
+
emitWorkerEventBestEffort({
|
|
246
226
|
adapter,
|
|
247
227
|
binding: nextBinding,
|
|
248
228
|
agent: this.name,
|
|
249
|
-
sessionId: result?.sessionId ||
|
|
229
|
+
sessionId: result?.sessionId || targetSessionId || binding.id,
|
|
250
230
|
cwd: result?.cwd || agentHome,
|
|
251
231
|
replyToMessageId: inbound.messageId || null,
|
|
252
232
|
event: {
|
|
@@ -257,12 +237,11 @@ export const openCodeRuntime = {
|
|
|
257
237
|
});
|
|
258
238
|
return true;
|
|
259
239
|
} catch (err) {
|
|
260
|
-
|
|
261
|
-
await emitWorkerEvent({
|
|
240
|
+
emitWorkerEventBestEffort({
|
|
262
241
|
adapter,
|
|
263
242
|
binding,
|
|
264
243
|
agent: this.name,
|
|
265
|
-
sessionId:
|
|
244
|
+
sessionId: targetSessionId || binding.id,
|
|
266
245
|
cwd: agentHome,
|
|
267
246
|
replyToMessageId: inbound.messageId || null,
|
|
268
247
|
event: {
|
|
@@ -290,7 +269,7 @@ export const openCodeRuntime = {
|
|
|
290
269
|
|
|
291
270
|
async reconcileAfterRestart(binding, ctx) {
|
|
292
271
|
const meta = binding.runtimeMeta || {};
|
|
293
|
-
|
|
272
|
+
emitWorkerEventBestEffort({
|
|
294
273
|
adapter: ctx.adapter,
|
|
295
274
|
binding,
|
|
296
275
|
agent: this.name,
|
|
@@ -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
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { basename } from 'node:path';
|
|
8
8
|
import { buildAgentRuntimeEnv } from '../../core/runtime-env.mjs';
|
|
9
|
+
import { getGoalTaskProtocolOverlayKey } from '../_shared/goal-task-protocol.mjs';
|
|
9
10
|
import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
|
|
10
11
|
import { ensureAgentHome } from '../../core/agent-home.mjs';
|
|
11
12
|
import {
|
|
@@ -18,17 +19,18 @@ import {
|
|
|
18
19
|
requirePiPath,
|
|
19
20
|
runPiPrompt,
|
|
20
21
|
} from './session.mjs';
|
|
21
|
-
import {
|
|
22
|
+
import { emitWorkerEventBestEffort } from '../../core/events/worker-events.mjs';
|
|
22
23
|
import {
|
|
23
24
|
shouldStreamRuntime,
|
|
24
|
-
createDeltaAggregator,
|
|
25
|
-
sendAdapterMessage,
|
|
26
|
-
recordActivity,
|
|
27
25
|
reportSubprocessFailure,
|
|
28
26
|
terminalRuntimeFailure,
|
|
29
27
|
updateBindingRuntimeMeta,
|
|
28
|
+
resolveRuntimeSessionScope,
|
|
29
|
+
buildRuntimeSessionMetaPatch,
|
|
30
30
|
} from '../../core/runtime-support.mjs';
|
|
31
31
|
|
|
32
|
+
const standingPromptSeen = new Set();
|
|
33
|
+
|
|
32
34
|
export const piRuntime = {
|
|
33
35
|
name: 'pi',
|
|
34
36
|
|
|
@@ -41,6 +43,7 @@ export const piRuntime = {
|
|
|
41
43
|
piPath,
|
|
42
44
|
agentEnv,
|
|
43
45
|
standingPrompt: opts.standingPrompt || null,
|
|
46
|
+
forceStandingPrompt: Boolean(opts.forceStandingPrompt),
|
|
44
47
|
timeoutMs: opts.timeoutMs,
|
|
45
48
|
});
|
|
46
49
|
},
|
|
@@ -54,6 +57,7 @@ export const piRuntime = {
|
|
|
54
57
|
piPath,
|
|
55
58
|
agentEnv,
|
|
56
59
|
standingPrompt: opts.standingPrompt || null,
|
|
60
|
+
forceStandingPrompt: Boolean(opts.forceStandingPrompt),
|
|
57
61
|
timeoutMs: opts.timeoutMs,
|
|
58
62
|
onEvent: opts.onEvent,
|
|
59
63
|
});
|
|
@@ -122,65 +126,47 @@ export const piRuntime = {
|
|
|
122
126
|
images = await buildPiImagesFromInbound(inbound);
|
|
123
127
|
const captionText = (inbound.text || '').trim();
|
|
124
128
|
if (images.length === 0 && !captionText) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
media: [],
|
|
129
|
-
replyToMessageId: inbound.messageId || null,
|
|
130
|
-
});
|
|
129
|
+
// Image decode failed and no caption to fall back on. Bail
|
|
130
|
+
// without a user notice; the dead chat-projection path that
|
|
131
|
+
// used to surface such notices is gone.
|
|
131
132
|
return true;
|
|
132
133
|
}
|
|
133
|
-
if (images.length === 0 && captionText) {
|
|
134
|
-
await sendAdapterMessage(adapter, binding, {
|
|
135
|
-
type: 'assistant',
|
|
136
|
-
text: '⚠️ Could not access the attached image data; acting on the caption text only.',
|
|
137
|
-
media: [],
|
|
138
|
-
replyToMessageId: inbound.messageId || null,
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
134
|
message = captionText || 'Please analyze the attached image(s).';
|
|
142
135
|
}
|
|
143
136
|
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
await emitWorkerEvent({
|
|
148
|
-
adapter,
|
|
149
|
-
binding,
|
|
150
|
-
agent: this.name,
|
|
151
|
-
sessionId: sessionId || meta.sessionId || binding.id,
|
|
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
|
-
});
|
|
137
|
+
const sessionScope = resolveRuntimeSessionScope(meta, inbound);
|
|
138
|
+
const shouldRotate = sessionScope.shouldRotate;
|
|
139
|
+
const targetSessionId = shouldRotate ? null : sessionScope.sessionId;
|
|
163
140
|
|
|
164
141
|
try {
|
|
165
142
|
const runtimePiPath = requirePiPath(meta.piPath || meta.runtimePath);
|
|
166
143
|
const runtimePiVersion = getPiRuntimeHealth(runtimePiPath).version || meta.piVersion || null;
|
|
167
144
|
const agentEnv = buildAgentRuntimeEnv({
|
|
168
145
|
agentId: binding.id,
|
|
169
|
-
sessionId:
|
|
146
|
+
sessionId: targetSessionId,
|
|
170
147
|
hostId: binding.runtime_host_id,
|
|
148
|
+
conversationId: inbound.conversationId,
|
|
149
|
+
messageId: inbound.messageId,
|
|
150
|
+
target: inbound.envelopeTarget,
|
|
171
151
|
});
|
|
172
|
-
const standingPrompt = buildStandingPrompt({ agentId: binding.id });
|
|
152
|
+
const standingPrompt = buildStandingPrompt({ agentId: binding.id, inbound });
|
|
153
|
+
const protocolOverlayKey = getGoalTaskProtocolOverlayKey({ agentId: binding.id, inbound });
|
|
154
|
+
const standingPromptSeenKey = targetSessionId
|
|
155
|
+
? `${binding.id}|${targetSessionId}|${protocolOverlayKey}`
|
|
156
|
+
: null;
|
|
157
|
+
const forceStandingPrompt = Boolean(standingPromptSeenKey && !standingPromptSeen.has(standingPromptSeenKey));
|
|
173
158
|
const result = shouldStreamRuntime(this.name, this)
|
|
174
|
-
? await this.runTurnStream({ sessionId:
|
|
159
|
+
? await this.runTurnStream({ sessionId: targetSessionId, cwd: agentHome, piPath: runtimePiPath, agentEnv }, message, {
|
|
175
160
|
standingPrompt,
|
|
161
|
+
forceStandingPrompt,
|
|
176
162
|
images,
|
|
177
163
|
onEvent: async (event) => {
|
|
178
164
|
if (event?.type === 'turn.started') {
|
|
179
|
-
|
|
165
|
+
emitWorkerEventBestEffort({
|
|
180
166
|
adapter,
|
|
181
167
|
binding,
|
|
182
168
|
agent: this.name,
|
|
183
|
-
sessionId: event.sessionId ||
|
|
169
|
+
sessionId: event.sessionId || targetSessionId || binding.id,
|
|
184
170
|
cwd: agentHome,
|
|
185
171
|
replyToMessageId: inbound.messageId || null,
|
|
186
172
|
event: {
|
|
@@ -189,32 +175,29 @@ export const piRuntime = {
|
|
|
189
175
|
},
|
|
190
176
|
logger: ctx.logger,
|
|
191
177
|
});
|
|
192
|
-
} else if (event?.type === 'message.delta' && event.text) {
|
|
193
|
-
deltaAggregator.push(event.text, {
|
|
194
|
-
sessionId: event.sessionId || meta.sessionId || binding.id,
|
|
195
|
-
cwd: agentHome,
|
|
196
|
-
});
|
|
197
178
|
}
|
|
198
179
|
},
|
|
199
180
|
})
|
|
200
|
-
: await this.runTurn({ sessionId:
|
|
181
|
+
: await this.runTurn({ sessionId: targetSessionId, cwd: agentHome, piPath: runtimePiPath, agentEnv }, message, { images, standingPrompt, forceStandingPrompt });
|
|
201
182
|
|
|
202
|
-
|
|
183
|
+
const observedSessionId = result?.sessionId || targetSessionId;
|
|
184
|
+
if (observedSessionId) {
|
|
185
|
+
standingPromptSeen.add(`${binding.id}|${observedSessionId}|${protocolOverlayKey}`);
|
|
186
|
+
}
|
|
203
187
|
const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
|
|
204
|
-
|
|
205
|
-
|
|
188
|
+
...buildRuntimeSessionMetaPatch(meta, sessionScope, {
|
|
189
|
+
sessionId: result?.sessionId,
|
|
190
|
+
path: result?.path,
|
|
191
|
+
}),
|
|
206
192
|
runtimePath: runtimePiPath,
|
|
207
193
|
piPath: runtimePiPath,
|
|
208
194
|
piVersion: runtimePiVersion,
|
|
209
|
-
rotatePending: false,
|
|
210
|
-
lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
|
|
211
195
|
}, { status: 'connected' });
|
|
212
|
-
|
|
213
|
-
await emitWorkerEvent({
|
|
196
|
+
emitWorkerEventBestEffort({
|
|
214
197
|
adapter,
|
|
215
198
|
binding: nextBinding,
|
|
216
199
|
agent: this.name,
|
|
217
|
-
sessionId: result?.sessionId ||
|
|
200
|
+
sessionId: result?.sessionId || targetSessionId || binding.id,
|
|
218
201
|
cwd: result?.cwd || agentHome,
|
|
219
202
|
replyToMessageId: inbound.messageId || null,
|
|
220
203
|
event: {
|
|
@@ -225,12 +208,11 @@ export const piRuntime = {
|
|
|
225
208
|
});
|
|
226
209
|
return true;
|
|
227
210
|
} catch (err) {
|
|
228
|
-
|
|
229
|
-
await emitWorkerEvent({
|
|
211
|
+
emitWorkerEventBestEffort({
|
|
230
212
|
adapter,
|
|
231
213
|
binding,
|
|
232
214
|
agent: this.name,
|
|
233
|
-
sessionId:
|
|
215
|
+
sessionId: targetSessionId || binding.id,
|
|
234
216
|
cwd: agentHome,
|
|
235
217
|
replyToMessageId: inbound.messageId || null,
|
|
236
218
|
event: {
|
|
@@ -258,7 +240,7 @@ export const piRuntime = {
|
|
|
258
240
|
|
|
259
241
|
async reconcileAfterRestart(binding, ctx) {
|
|
260
242
|
const meta = binding.runtimeMeta || {};
|
|
261
|
-
|
|
243
|
+
emitWorkerEventBestEffort({
|
|
262
244
|
adapter: ctx.adapter,
|
|
263
245
|
binding,
|
|
264
246
|
agent: this.name,
|
|
@@ -197,11 +197,14 @@ export function runPiPrompt({
|
|
|
197
197
|
piPath = null,
|
|
198
198
|
agentEnv = null,
|
|
199
199
|
standingPrompt = null,
|
|
200
|
+
forceStandingPrompt = false,
|
|
200
201
|
timeoutMs = Number(process.env.PI_RUN_TIMEOUT_MS || DEFAULT_PI_RUN_TIMEOUT_MS),
|
|
201
202
|
onEvent,
|
|
202
203
|
}) {
|
|
203
|
-
// pi has no documented system-prompt flag
|
|
204
|
-
|
|
204
|
+
// pi has no documented system-prompt flag. Prepend on first turn, and
|
|
205
|
+
// let callers force one resumed-session injection for a newly selected
|
|
206
|
+
// protocol overlay.
|
|
207
|
+
const finalMessage = standingPrompt && (!sessionId || forceStandingPrompt)
|
|
205
208
|
? `${standingPrompt}\n\n---\n\n${message}`
|
|
206
209
|
: message;
|
|
207
210
|
return new Promise((resolve, reject) => {
|
|
@@ -220,12 +223,13 @@ export function runPiPrompt({
|
|
|
220
223
|
let activeSessionFile = null;
|
|
221
224
|
let finalText = '';
|
|
222
225
|
let settled = false;
|
|
223
|
-
let eventChain = Promise.resolve();
|
|
224
226
|
const pending = new Map();
|
|
225
227
|
|
|
226
228
|
const emit = (event) => {
|
|
227
229
|
if (typeof onEvent !== 'function') return;
|
|
228
|
-
|
|
230
|
+
void Promise.resolve()
|
|
231
|
+
.then(() => onEvent(event))
|
|
232
|
+
.catch(() => {});
|
|
229
233
|
};
|
|
230
234
|
|
|
231
235
|
const settle = (fn, value) => {
|
|
@@ -237,7 +241,7 @@ export function runPiPrompt({
|
|
|
237
241
|
}
|
|
238
242
|
pending.clear();
|
|
239
243
|
try { child.kill('SIGTERM'); } catch {}
|
|
240
|
-
|
|
244
|
+
fn(value);
|
|
241
245
|
};
|
|
242
246
|
|
|
243
247
|
const sendRaw = (payload) => {
|
|
@@ -305,7 +309,6 @@ export function runPiPrompt({
|
|
|
305
309
|
const delta = extractDeltaFromEvent(event);
|
|
306
310
|
if (delta) {
|
|
307
311
|
finalText += delta;
|
|
308
|
-
emit({ type: 'message.delta', sessionId: activeSessionId, text: delta });
|
|
309
312
|
}
|
|
310
313
|
return;
|
|
311
314
|
}
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ticlawk adapter — final agent message / media write path.
|
|
3
|
-
*
|
|
4
|
-
* Everything here turns an agent result (text + optional media local paths)
|
|
5
|
-
* into a terminal runtime result written back to ticlawk.
|
|
6
|
-
*
|
|
7
|
-
* This module imports from `./api.mjs` (HTTP client) and from
|
|
8
|
-
* `../../core/logger.mjs` (structured logging). No runtime dependencies.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
12
|
-
import { extname } from 'node:path';
|
|
13
|
-
import { randomUUID } from 'node:crypto';
|
|
14
|
-
import * as api from './api.mjs';
|
|
15
|
-
import { extractMediaPaths } from '../../core/media/outbound.mjs';
|
|
16
|
-
import { debugLog, debugError } from '../../core/logger.mjs';
|
|
17
|
-
import { TICLAWK_CONNECTOR_API_KEY } from '../../core/config.mjs';
|
|
18
|
-
|
|
19
|
-
const MIME_MAP = {
|
|
20
|
-
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
21
|
-
'.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml',
|
|
22
|
-
'.mp4': 'video/mp4', '.webm': 'video/webm',
|
|
23
|
-
'.opus': 'audio/opus', '.mp3': 'audio/mpeg', '.wav': 'audio/wav',
|
|
24
|
-
'.pdf': 'application/pdf',
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
async function uploadLocalAsset(localPath) {
|
|
28
|
-
if (!process.env[TICLAWK_CONNECTOR_API_KEY]) {
|
|
29
|
-
debugError('relay', 'upload.skipped', {
|
|
30
|
-
localPath,
|
|
31
|
-
reason: 'missing connector api key',
|
|
32
|
-
});
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
if (!existsSync(localPath)) {
|
|
36
|
-
debugError('relay', 'upload.skipped', {
|
|
37
|
-
localPath,
|
|
38
|
-
reason: 'file not found',
|
|
39
|
-
});
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const ext = extname(localPath).toLowerCase();
|
|
44
|
-
const contentType = MIME_MAP[ext] || 'application/octet-stream';
|
|
45
|
-
const fileName = `${Date.now()}-${randomUUID().slice(0, 8)}${ext}`;
|
|
46
|
-
const fileData = readFileSync(localPath);
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
const asset = await api.uploadAsset(fileName, fileData, contentType);
|
|
50
|
-
if (!asset?.asset_id) {
|
|
51
|
-
debugError('relay', 'upload.failed', {
|
|
52
|
-
localPath,
|
|
53
|
-
fileName,
|
|
54
|
-
error: 'missing asset_id in upload response',
|
|
55
|
-
});
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
debugLog('relay', 'upload.ok', {
|
|
59
|
-
localPath,
|
|
60
|
-
fileName,
|
|
61
|
-
assetId: asset.asset_id,
|
|
62
|
-
contentType: asset.content_type || contentType,
|
|
63
|
-
sizeBytes: asset.size_bytes ?? null,
|
|
64
|
-
});
|
|
65
|
-
return asset;
|
|
66
|
-
} catch (err) {
|
|
67
|
-
debugError('relay', 'upload.failed', {
|
|
68
|
-
localPath,
|
|
69
|
-
fileName,
|
|
70
|
-
error: err.message,
|
|
71
|
-
});
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export async function uploadMediaAssets(localPaths) {
|
|
77
|
-
const assets = [];
|
|
78
|
-
for (const p of localPaths) {
|
|
79
|
-
const asset = await uploadLocalAsset(p);
|
|
80
|
-
if (asset) assets.push(asset);
|
|
81
|
-
}
|
|
82
|
-
return assets;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export async function processAndSaveResult(result, opts) {
|
|
86
|
-
const { agentId: explicitAgentId, sessionKey, hostId, type, replyToMessageId, agent, sessionId, turnId, runtimeVersion } = opts;
|
|
87
|
-
const agentId = explicitAgentId || sessionKey;
|
|
88
|
-
const startedAt = Date.now();
|
|
89
|
-
|
|
90
|
-
// Collect media: from agent mediaUrls + parsed from text
|
|
91
|
-
const allLocalPaths = [...new Set([
|
|
92
|
-
...(result.mediaUrls || []),
|
|
93
|
-
...extractMediaPaths(result.text || ''),
|
|
94
|
-
])];
|
|
95
|
-
|
|
96
|
-
debugLog('relay', 'process-result.begin', {
|
|
97
|
-
agentId,
|
|
98
|
-
type,
|
|
99
|
-
parentMessageId: replyToMessageId || null,
|
|
100
|
-
textLength: result.text?.length || 0,
|
|
101
|
-
localMediaCount: allLocalPaths.length,
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
// Upload local media to Ticlawk private chat assets.
|
|
105
|
-
const uploadedAssets = await uploadMediaAssets(allLocalPaths);
|
|
106
|
-
const uploadedAssetIds = uploadedAssets
|
|
107
|
-
.map((asset) => asset?.asset_id)
|
|
108
|
-
.filter(Boolean);
|
|
109
|
-
|
|
110
|
-
const updateId = randomUUID();
|
|
111
|
-
debugLog('relay', 'post-final.begin', {
|
|
112
|
-
agentId,
|
|
113
|
-
updateId,
|
|
114
|
-
durationMs: Date.now() - startedAt,
|
|
115
|
-
uploadedMediaCount: uploadedAssetIds.length,
|
|
116
|
-
});
|
|
117
|
-
try {
|
|
118
|
-
await api.postRuntimeResult({
|
|
119
|
-
agent,
|
|
120
|
-
agent_id: agentId,
|
|
121
|
-
runtime_host_id: hostId,
|
|
122
|
-
session_id: sessionId || null,
|
|
123
|
-
cwd: '',
|
|
124
|
-
runtime_version: runtimeVersion ?? null,
|
|
125
|
-
result_id: updateId,
|
|
126
|
-
turn_id: turnId || replyToMessageId || null,
|
|
127
|
-
reply_to_message_id: replyToMessageId || null,
|
|
128
|
-
origin_ts: new Date().toISOString(),
|
|
129
|
-
text: result.text || '',
|
|
130
|
-
media_asset_ids: uploadedAssetIds,
|
|
131
|
-
output_type: type || 'agent_message',
|
|
132
|
-
});
|
|
133
|
-
} catch (err) {
|
|
134
|
-
debugError('relay', 'post-final.failed', {
|
|
135
|
-
agentId,
|
|
136
|
-
updateId,
|
|
137
|
-
durationMs: Date.now() - startedAt,
|
|
138
|
-
error: err.message,
|
|
139
|
-
});
|
|
140
|
-
throw err;
|
|
141
|
-
}
|
|
142
|
-
debugLog('relay', 'process-result.ok', {
|
|
143
|
-
agentId,
|
|
144
|
-
updateId,
|
|
145
|
-
durationMs: Date.now() - startedAt,
|
|
146
|
-
uploadedMediaCount: uploadedAssetIds.length,
|
|
147
|
-
});
|
|
148
|
-
return { id: updateId, agentId };
|
|
149
|
-
}
|