ticlawk 0.1.16-dev.2 → 0.1.16-dev.21
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 +230 -22
- package/src/adapters/ticlawk/credentials.mjs +41 -1
- package/src/adapters/ticlawk/index.mjs +196 -195
- package/src/adapters/ticlawk/wake-client.mjs +1 -1
- package/src/cli/agent-commands.mjs +601 -35
- package/src/core/agent-cli-handlers.mjs +448 -20
- package/src/core/agent-home.mjs +50 -10
- package/src/core/argv.mjs +11 -1
- package/src/core/http.mjs +126 -0
- package/src/core/runtime-env.mjs +7 -0
- package/src/core/runtime-support.mjs +101 -30
- package/src/migrate/write-initial-memory.mjs +5 -5
- package/src/runtimes/_shared/brand.mjs +1 -0
- package/src/runtimes/_shared/goal-task-protocol.mjs +228 -0
- package/src/runtimes/_shared/standing-prompt.mjs +120 -278
- package/src/runtimes/_shared/wake-prompt.mjs +173 -0
- package/src/runtimes/claude-code/index.mjs +30 -108
- package/src/runtimes/codex/index.mjs +114 -23
- package/src/runtimes/openclaw/index.mjs +16 -26
- package/src/runtimes/opencode/index.mjs +42 -36
- package/src/runtimes/opencode/session.mjs +5 -4
- package/src/runtimes/pi/index.mjs +39 -31
- package/src/runtimes/pi/session.mjs +5 -2
- 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,19 @@ import {
|
|
|
24
25
|
requireOpenCodePath,
|
|
25
26
|
} from './session.mjs';
|
|
26
27
|
import { buildOpenCodeInputFromInbound } from '../../core/media/inbound.mjs';
|
|
27
|
-
import { normalizeOutboundMedia } from '../../core/media/outbound.mjs';
|
|
28
28
|
import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
|
|
29
29
|
import {
|
|
30
30
|
shouldStreamRuntime,
|
|
31
31
|
createDeltaAggregator,
|
|
32
|
-
sendAdapterMessage,
|
|
33
|
-
recordActivity,
|
|
34
32
|
reportSubprocessFailure,
|
|
35
33
|
terminalRuntimeFailure,
|
|
36
34
|
updateBindingRuntimeMeta,
|
|
35
|
+
resolveRuntimeSessionScope,
|
|
36
|
+
buildRuntimeSessionMetaPatch,
|
|
37
37
|
} from '../../core/runtime-support.mjs';
|
|
38
38
|
|
|
39
|
+
const standingPromptSeen = new Set();
|
|
40
|
+
|
|
39
41
|
export const openCodeRuntime = {
|
|
40
42
|
name: 'opencode',
|
|
41
43
|
|
|
@@ -51,6 +53,7 @@ export const openCodeRuntime = {
|
|
|
51
53
|
opencodePath,
|
|
52
54
|
agentEnv,
|
|
53
55
|
standingPrompt: opts.standingPrompt || null,
|
|
56
|
+
forceStandingPrompt: Boolean(opts.forceStandingPrompt),
|
|
54
57
|
files: opts.files,
|
|
55
58
|
timeoutMs: opts.timeoutMs,
|
|
56
59
|
});
|
|
@@ -64,6 +67,7 @@ export const openCodeRuntime = {
|
|
|
64
67
|
opencodePath,
|
|
65
68
|
agentEnv,
|
|
66
69
|
standingPrompt: opts.standingPrompt || null,
|
|
70
|
+
forceStandingPrompt: Boolean(opts.forceStandingPrompt),
|
|
67
71
|
files: opts.files,
|
|
68
72
|
timeoutMs: opts.timeoutMs,
|
|
69
73
|
onEvent: opts.onEvent,
|
|
@@ -146,30 +150,22 @@ export const openCodeRuntime = {
|
|
|
146
150
|
const captionText = (inbound.text || '').trim();
|
|
147
151
|
|
|
148
152
|
if (files.length === 0 && !captionText) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
replyToMessageId: inbound.messageId || null,
|
|
154
|
-
});
|
|
153
|
+
// Image decode failed and no caption to fall back on — we have
|
|
154
|
+
// nothing meaningful to feed the model. Bail without a user
|
|
155
|
+
// notice; this runtime is non-primary and the dead chat-projection
|
|
156
|
+
// path that used to surface such notices is gone.
|
|
155
157
|
return true;
|
|
156
158
|
}
|
|
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
|
-
}
|
|
159
|
+
// If files.length === 0 && captionText, fall through with the
|
|
160
|
+
// caption-only message below — no inline user notice.
|
|
167
161
|
|
|
168
162
|
// If user sent images with no caption, give the model a minimal
|
|
169
163
|
// instruction so it has something to anchor on.
|
|
170
164
|
message = captionText || 'Please analyze the attached image(s).';
|
|
171
165
|
}
|
|
172
|
-
const
|
|
166
|
+
const sessionScope = resolveRuntimeSessionScope(meta, inbound);
|
|
167
|
+
const shouldRotate = sessionScope.shouldRotate;
|
|
168
|
+
const targetSessionId = shouldRotate ? null : sessionScope.sessionId;
|
|
173
169
|
|
|
174
170
|
const deltaAggregator = createDeltaAggregator({
|
|
175
171
|
flushDelta: async ({ text, sessionId, cwd }) => {
|
|
@@ -177,7 +173,7 @@ export const openCodeRuntime = {
|
|
|
177
173
|
adapter,
|
|
178
174
|
binding,
|
|
179
175
|
agent: this.name,
|
|
180
|
-
sessionId: sessionId ||
|
|
176
|
+
sessionId: sessionId || targetSessionId || binding.id,
|
|
181
177
|
cwd: cwd || agentHome,
|
|
182
178
|
replyToMessageId: inbound.messageId || null,
|
|
183
179
|
event: {
|
|
@@ -195,13 +191,22 @@ export const openCodeRuntime = {
|
|
|
195
191
|
const opencodeVersion = getOpenCodeRuntimeHealth(opencodePath).version || meta.opencodeVersion || null;
|
|
196
192
|
const agentEnv = buildAgentRuntimeEnv({
|
|
197
193
|
agentId: binding.id,
|
|
198
|
-
sessionId:
|
|
194
|
+
sessionId: targetSessionId,
|
|
199
195
|
hostId: binding.runtime_host_id,
|
|
196
|
+
conversationId: inbound.conversationId,
|
|
197
|
+
messageId: inbound.messageId,
|
|
198
|
+
target: inbound.envelopeTarget,
|
|
200
199
|
});
|
|
201
|
-
const standingPrompt = buildStandingPrompt({ agentId: binding.id });
|
|
200
|
+
const standingPrompt = buildStandingPrompt({ agentId: binding.id, inbound });
|
|
201
|
+
const protocolOverlayKey = getGoalTaskProtocolOverlayKey({ agentId: binding.id, inbound });
|
|
202
|
+
const standingPromptSeenKey = targetSessionId
|
|
203
|
+
? `${binding.id}|${targetSessionId}|${protocolOverlayKey}`
|
|
204
|
+
: null;
|
|
205
|
+
const forceStandingPrompt = Boolean(standingPromptSeenKey && !standingPromptSeen.has(standingPromptSeenKey));
|
|
202
206
|
const result = shouldStreamRuntime(this.name, this)
|
|
203
|
-
? await this.runTurnStream({ sessionId:
|
|
207
|
+
? await this.runTurnStream({ sessionId: targetSessionId, cwd: agentHome, opencodePath, agentEnv }, message, {
|
|
204
208
|
standingPrompt,
|
|
209
|
+
forceStandingPrompt,
|
|
205
210
|
files,
|
|
206
211
|
onEvent: async (event) => {
|
|
207
212
|
if (event?.type === 'turn.started') {
|
|
@@ -209,7 +214,7 @@ export const openCodeRuntime = {
|
|
|
209
214
|
adapter,
|
|
210
215
|
binding,
|
|
211
216
|
agent: this.name,
|
|
212
|
-
sessionId: event.sessionId ||
|
|
217
|
+
sessionId: event.sessionId || targetSessionId || binding.id,
|
|
213
218
|
cwd: agentHome,
|
|
214
219
|
replyToMessageId: inbound.messageId || null,
|
|
215
220
|
event: {
|
|
@@ -220,33 +225,34 @@ export const openCodeRuntime = {
|
|
|
220
225
|
});
|
|
221
226
|
} else if (event?.type === 'message.delta' && event.text) {
|
|
222
227
|
deltaAggregator.push(event.text, {
|
|
223
|
-
sessionId: event.sessionId ||
|
|
228
|
+
sessionId: event.sessionId || targetSessionId || binding.id,
|
|
224
229
|
cwd: agentHome,
|
|
225
230
|
});
|
|
226
231
|
}
|
|
227
232
|
},
|
|
228
233
|
})
|
|
229
|
-
: await this.runTurn({ sessionId:
|
|
234
|
+
: await this.runTurn({ sessionId: targetSessionId, cwd: agentHome, opencodePath, agentEnv }, message, { files, standingPrompt, forceStandingPrompt });
|
|
230
235
|
|
|
231
236
|
await deltaAggregator.flush();
|
|
237
|
+
const observedSessionId = result?.sessionId || targetSessionId;
|
|
238
|
+
if (observedSessionId) {
|
|
239
|
+
standingPromptSeen.add(`${binding.id}|${observedSessionId}|${protocolOverlayKey}`);
|
|
240
|
+
}
|
|
232
241
|
|
|
233
242
|
const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
|
|
234
|
-
|
|
243
|
+
...buildRuntimeSessionMetaPatch(meta, sessionScope, {
|
|
244
|
+
sessionId: result?.sessionId,
|
|
245
|
+
path: result?.path,
|
|
246
|
+
}),
|
|
235
247
|
runtimePath: opencodePath,
|
|
236
248
|
opencodePath,
|
|
237
249
|
opencodeVersion,
|
|
238
|
-
rotatePending: false,
|
|
239
|
-
lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
|
|
240
250
|
}, { status: 'connected' });
|
|
241
|
-
await recordActivity(adapter, nextBinding, inbound, {
|
|
242
|
-
...result,
|
|
243
|
-
media: normalizeOutboundMedia(result),
|
|
244
|
-
});
|
|
245
251
|
await emitWorkerEvent({
|
|
246
252
|
adapter,
|
|
247
253
|
binding: nextBinding,
|
|
248
254
|
agent: this.name,
|
|
249
|
-
sessionId: result?.sessionId ||
|
|
255
|
+
sessionId: result?.sessionId || targetSessionId || binding.id,
|
|
250
256
|
cwd: result?.cwd || agentHome,
|
|
251
257
|
replyToMessageId: inbound.messageId || null,
|
|
252
258
|
event: {
|
|
@@ -262,7 +268,7 @@ export const openCodeRuntime = {
|
|
|
262
268
|
adapter,
|
|
263
269
|
binding,
|
|
264
270
|
agent: this.name,
|
|
265
|
-
sessionId:
|
|
271
|
+
sessionId: targetSessionId || binding.id,
|
|
266
272
|
cwd: agentHome,
|
|
267
273
|
replyToMessageId: inbound.messageId || null,
|
|
268
274
|
event: {
|
|
@@ -210,14 +210,15 @@ export function runOpenCodePrompt({
|
|
|
210
210
|
opencodePath = null,
|
|
211
211
|
agentEnv = null,
|
|
212
212
|
standingPrompt = null,
|
|
213
|
+
forceStandingPrompt = false,
|
|
213
214
|
timeoutMs = Number(process.env.OPENCODE_RUN_TIMEOUT_MS || DEFAULT_OPENCODE_RUN_TIMEOUT_MS),
|
|
214
215
|
onEvent,
|
|
215
216
|
}) {
|
|
216
217
|
// 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
|
|
218
|
+
// standing prompt to the first-turn message body. Callers may force a
|
|
219
|
+
// resumed-session injection when the selected protocol overlay changes
|
|
220
|
+
// or this daemon process has not yet observed the session.
|
|
221
|
+
const finalMessage = standingPrompt && (!sessionId || forceStandingPrompt)
|
|
221
222
|
? `${standingPrompt}\n\n---\n\n${message}`
|
|
222
223
|
: message;
|
|
223
224
|
return new Promise((resolve, reject) => {
|
|
@@ -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 {
|
|
@@ -22,13 +23,15 @@ import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
|
|
|
22
23
|
import {
|
|
23
24
|
shouldStreamRuntime,
|
|
24
25
|
createDeltaAggregator,
|
|
25
|
-
sendAdapterMessage,
|
|
26
|
-
recordActivity,
|
|
27
26
|
reportSubprocessFailure,
|
|
28
27
|
terminalRuntimeFailure,
|
|
29
28
|
updateBindingRuntimeMeta,
|
|
29
|
+
resolveRuntimeSessionScope,
|
|
30
|
+
buildRuntimeSessionMetaPatch,
|
|
30
31
|
} from '../../core/runtime-support.mjs';
|
|
31
32
|
|
|
33
|
+
const standingPromptSeen = new Set();
|
|
34
|
+
|
|
32
35
|
export const piRuntime = {
|
|
33
36
|
name: 'pi',
|
|
34
37
|
|
|
@@ -41,6 +44,7 @@ export const piRuntime = {
|
|
|
41
44
|
piPath,
|
|
42
45
|
agentEnv,
|
|
43
46
|
standingPrompt: opts.standingPrompt || null,
|
|
47
|
+
forceStandingPrompt: Boolean(opts.forceStandingPrompt),
|
|
44
48
|
timeoutMs: opts.timeoutMs,
|
|
45
49
|
});
|
|
46
50
|
},
|
|
@@ -54,6 +58,7 @@ export const piRuntime = {
|
|
|
54
58
|
piPath,
|
|
55
59
|
agentEnv,
|
|
56
60
|
standingPrompt: opts.standingPrompt || null,
|
|
61
|
+
forceStandingPrompt: Boolean(opts.forceStandingPrompt),
|
|
57
62
|
timeoutMs: opts.timeoutMs,
|
|
58
63
|
onEvent: opts.onEvent,
|
|
59
64
|
});
|
|
@@ -122,33 +127,24 @@ export const piRuntime = {
|
|
|
122
127
|
images = await buildPiImagesFromInbound(inbound);
|
|
123
128
|
const captionText = (inbound.text || '').trim();
|
|
124
129
|
if (images.length === 0 && !captionText) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
media: [],
|
|
129
|
-
replyToMessageId: inbound.messageId || null,
|
|
130
|
-
});
|
|
130
|
+
// Image decode failed and no caption to fall back on. Bail
|
|
131
|
+
// without a user notice; the dead chat-projection path that
|
|
132
|
+
// used to surface such notices is gone.
|
|
131
133
|
return true;
|
|
132
134
|
}
|
|
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
135
|
message = captionText || 'Please analyze the attached image(s).';
|
|
142
136
|
}
|
|
143
137
|
|
|
144
|
-
const
|
|
138
|
+
const sessionScope = resolveRuntimeSessionScope(meta, inbound);
|
|
139
|
+
const shouldRotate = sessionScope.shouldRotate;
|
|
140
|
+
const targetSessionId = shouldRotate ? null : sessionScope.sessionId;
|
|
145
141
|
const deltaAggregator = createDeltaAggregator({
|
|
146
142
|
flushDelta: async ({ text, sessionId, cwd }) => {
|
|
147
143
|
await emitWorkerEvent({
|
|
148
144
|
adapter,
|
|
149
145
|
binding,
|
|
150
146
|
agent: this.name,
|
|
151
|
-
sessionId: sessionId ||
|
|
147
|
+
sessionId: sessionId || targetSessionId || binding.id,
|
|
152
148
|
cwd: cwd || agentHome,
|
|
153
149
|
replyToMessageId: inbound.messageId || null,
|
|
154
150
|
event: {
|
|
@@ -166,13 +162,22 @@ export const piRuntime = {
|
|
|
166
162
|
const runtimePiVersion = getPiRuntimeHealth(runtimePiPath).version || meta.piVersion || null;
|
|
167
163
|
const agentEnv = buildAgentRuntimeEnv({
|
|
168
164
|
agentId: binding.id,
|
|
169
|
-
sessionId:
|
|
165
|
+
sessionId: targetSessionId,
|
|
170
166
|
hostId: binding.runtime_host_id,
|
|
167
|
+
conversationId: inbound.conversationId,
|
|
168
|
+
messageId: inbound.messageId,
|
|
169
|
+
target: inbound.envelopeTarget,
|
|
171
170
|
});
|
|
172
|
-
const standingPrompt = buildStandingPrompt({ agentId: binding.id });
|
|
171
|
+
const standingPrompt = buildStandingPrompt({ agentId: binding.id, inbound });
|
|
172
|
+
const protocolOverlayKey = getGoalTaskProtocolOverlayKey({ agentId: binding.id, inbound });
|
|
173
|
+
const standingPromptSeenKey = targetSessionId
|
|
174
|
+
? `${binding.id}|${targetSessionId}|${protocolOverlayKey}`
|
|
175
|
+
: null;
|
|
176
|
+
const forceStandingPrompt = Boolean(standingPromptSeenKey && !standingPromptSeen.has(standingPromptSeenKey));
|
|
173
177
|
const result = shouldStreamRuntime(this.name, this)
|
|
174
|
-
? await this.runTurnStream({ sessionId:
|
|
178
|
+
? await this.runTurnStream({ sessionId: targetSessionId, cwd: agentHome, piPath: runtimePiPath, agentEnv }, message, {
|
|
175
179
|
standingPrompt,
|
|
180
|
+
forceStandingPrompt,
|
|
176
181
|
images,
|
|
177
182
|
onEvent: async (event) => {
|
|
178
183
|
if (event?.type === 'turn.started') {
|
|
@@ -180,7 +185,7 @@ export const piRuntime = {
|
|
|
180
185
|
adapter,
|
|
181
186
|
binding,
|
|
182
187
|
agent: this.name,
|
|
183
|
-
sessionId: event.sessionId ||
|
|
188
|
+
sessionId: event.sessionId || targetSessionId || binding.id,
|
|
184
189
|
cwd: agentHome,
|
|
185
190
|
replyToMessageId: inbound.messageId || null,
|
|
186
191
|
event: {
|
|
@@ -191,30 +196,33 @@ export const piRuntime = {
|
|
|
191
196
|
});
|
|
192
197
|
} else if (event?.type === 'message.delta' && event.text) {
|
|
193
198
|
deltaAggregator.push(event.text, {
|
|
194
|
-
sessionId: event.sessionId ||
|
|
199
|
+
sessionId: event.sessionId || targetSessionId || binding.id,
|
|
195
200
|
cwd: agentHome,
|
|
196
201
|
});
|
|
197
202
|
}
|
|
198
203
|
},
|
|
199
204
|
})
|
|
200
|
-
: await this.runTurn({ sessionId:
|
|
205
|
+
: await this.runTurn({ sessionId: targetSessionId, cwd: agentHome, piPath: runtimePiPath, agentEnv }, message, { images, standingPrompt, forceStandingPrompt });
|
|
201
206
|
|
|
202
207
|
await deltaAggregator.flush();
|
|
208
|
+
const observedSessionId = result?.sessionId || targetSessionId;
|
|
209
|
+
if (observedSessionId) {
|
|
210
|
+
standingPromptSeen.add(`${binding.id}|${observedSessionId}|${protocolOverlayKey}`);
|
|
211
|
+
}
|
|
203
212
|
const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
|
|
204
|
-
|
|
205
|
-
|
|
213
|
+
...buildRuntimeSessionMetaPatch(meta, sessionScope, {
|
|
214
|
+
sessionId: result?.sessionId,
|
|
215
|
+
path: result?.path,
|
|
216
|
+
}),
|
|
206
217
|
runtimePath: runtimePiPath,
|
|
207
218
|
piPath: runtimePiPath,
|
|
208
219
|
piVersion: runtimePiVersion,
|
|
209
|
-
rotatePending: false,
|
|
210
|
-
lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
|
|
211
220
|
}, { status: 'connected' });
|
|
212
|
-
await recordActivity(adapter, nextBinding, inbound, result);
|
|
213
221
|
await emitWorkerEvent({
|
|
214
222
|
adapter,
|
|
215
223
|
binding: nextBinding,
|
|
216
224
|
agent: this.name,
|
|
217
|
-
sessionId: result?.sessionId ||
|
|
225
|
+
sessionId: result?.sessionId || targetSessionId || binding.id,
|
|
218
226
|
cwd: result?.cwd || agentHome,
|
|
219
227
|
replyToMessageId: inbound.messageId || null,
|
|
220
228
|
event: {
|
|
@@ -230,7 +238,7 @@ export const piRuntime = {
|
|
|
230
238
|
adapter,
|
|
231
239
|
binding,
|
|
232
240
|
agent: this.name,
|
|
233
|
-
sessionId:
|
|
241
|
+
sessionId: targetSessionId || binding.id,
|
|
234
242
|
cwd: agentHome,
|
|
235
243
|
replyToMessageId: inbound.messageId || null,
|
|
236
244
|
event: {
|
|
@@ -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) => {
|
|
@@ -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
|
-
}
|