ticlawk 0.1.16-dev.9 → 0.1.16
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 +15 -3
- package/bin/ticlawk.mjs +208 -21
- package/package.json +1 -1
- package/src/adapters/ticlawk/api.mjs +283 -48
- package/src/adapters/ticlawk/credentials.mjs +41 -1
- package/src/adapters/ticlawk/index.mjs +126 -121
- package/src/adapters/ticlawk/wake-client.mjs +1 -1
- package/src/cli/agent-commands.mjs +557 -18
- package/src/core/agent-cli-handlers.mjs +435 -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 +119 -0
- package/src/core/runtime-contract.mjs +0 -1
- package/src/core/runtime-env.mjs +7 -0
- package/src/core/runtime-support.mjs +108 -77
- 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 +55 -0
- package/src/runtimes/_shared/handbook/DM_SCOPE.md +13 -0
- package/src/runtimes/_shared/handbook/GOAL_AUTHORITY.md +46 -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 +134 -278
- package/src/runtimes/_shared/wake-prompt.mjs +261 -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
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-turn inbound wake prompt builder.
|
|
3
|
+
*
|
|
4
|
+
* Standing prompts define durable runtime behavior. This module owns the
|
|
5
|
+
* dynamic wrapper around each delivered Ticlawk message: who sent it, why it
|
|
6
|
+
* reached this agent, the exact reply target, and any attached goal/task/quote
|
|
7
|
+
* context for this turn.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
function promptBlock(text) {
|
|
11
|
+
return text.trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildEnvelopeTarget(msg) {
|
|
15
|
+
const convType = msg.conversation_type || 'dm';
|
|
16
|
+
const conversationId = msg.conversation_id || '';
|
|
17
|
+
const senderHandle = msg.sender_display_name || msg.sender_user_id || msg.sender_agent_id || '';
|
|
18
|
+
if (convType === 'dm') {
|
|
19
|
+
return senderHandle ? `dm:@${senderHandle}` : `dm:${conversationId}`;
|
|
20
|
+
}
|
|
21
|
+
if (convType === 'thread') {
|
|
22
|
+
const groupName = msg.conversation_name || conversationId;
|
|
23
|
+
const replyRoot = msg.thread_root_message_id || msg.message_id || '';
|
|
24
|
+
return `#${groupName}:${replyRoot}`;
|
|
25
|
+
}
|
|
26
|
+
// group
|
|
27
|
+
const groupName = msg.conversation_name || conversationId;
|
|
28
|
+
return `#${groupName}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function buildDebugTaskSuffix(msg) {
|
|
32
|
+
if (msg.task_number == null) return '';
|
|
33
|
+
const status = msg.task_status || 'todo';
|
|
34
|
+
const parts = [`task #${msg.task_number} status=${status}`];
|
|
35
|
+
if (msg.task_assignee_agent_id || msg.task_assignee_user_id) {
|
|
36
|
+
const t = msg.task_assignee_type || 'agent';
|
|
37
|
+
const id = msg.task_assignee_agent_id || msg.task_assignee_user_id;
|
|
38
|
+
parts.push(`assignee=${t}:${id}`);
|
|
39
|
+
}
|
|
40
|
+
if (msg.task_title) {
|
|
41
|
+
parts.push(`title=${JSON.stringify(msg.task_title)}`);
|
|
42
|
+
}
|
|
43
|
+
return ` [${parts.join(' ')}]`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function buildDebugReactionsSuffix(msg) {
|
|
47
|
+
const entries = Array.isArray(msg.reactions_summary) ? msg.reactions_summary : [];
|
|
48
|
+
if (entries.length === 0) return '';
|
|
49
|
+
return ` [reactions: ${entries.join('; ')}]`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeDeliveryReasonForPrompt(reason) {
|
|
53
|
+
return reason === 'thread_follow' ? 'reply_follow' : reason;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function buildDebugEnvelopeHeader(msg) {
|
|
57
|
+
const target = buildEnvelopeTarget(msg);
|
|
58
|
+
const msgId = msg.id || msg.message_id || '';
|
|
59
|
+
const seq = msg.seq != null ? msg.seq : '';
|
|
60
|
+
const time = msg.created_at || new Date().toISOString();
|
|
61
|
+
const type = msg.sender_type || 'human';
|
|
62
|
+
const sender = msg.sender_display_name || msg.sender_user_id || msg.sender_agent_id || 'unknown';
|
|
63
|
+
const convType = msg.conversation_type || 'dm';
|
|
64
|
+
const displayReason = normalizeDeliveryReasonForPrompt(msg.reason || '');
|
|
65
|
+
const reason = displayReason ? ` reason=${displayReason}` : '';
|
|
66
|
+
const recipientRole = String(msg.recipient_conversation_role || msg.recipient_role || '').trim().toLowerCase();
|
|
67
|
+
const role = convType === 'group' && recipientRole ? ` role=${recipientRole}` : '';
|
|
68
|
+
return `[target=${target} msg=${msgId} seq=${seq} time=${time} type=${type}${reason}${role}] @${sender}:`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const buildEnvelopeHeader = buildDebugEnvelopeHeader;
|
|
72
|
+
|
|
73
|
+
export function buildGroupContextBlock(msg) {
|
|
74
|
+
// Only useful in groups — DMs don't need group-purpose context.
|
|
75
|
+
if ((msg.conversation_type || 'dm') !== 'group') return '';
|
|
76
|
+
const lines = [];
|
|
77
|
+
const name = msg.conversation_name || msg.conversation_display_name || '';
|
|
78
|
+
if (name) lines.push(`Group: #${name}`);
|
|
79
|
+
const description = (msg.conversation_description || '').trim();
|
|
80
|
+
if (description) lines.push(`Purpose: ${description}`);
|
|
81
|
+
if (lines.length === 0) return '';
|
|
82
|
+
return lines.join('\n');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function buildCharterBlock(msg) {
|
|
86
|
+
const charter = (msg.conversation_charter || '').trim();
|
|
87
|
+
if (!charter) return '';
|
|
88
|
+
const authorityLine = hasGoalAuthority(msg)
|
|
89
|
+
? 'Use it as the current goal and role spec. If the owner appears to change the goal, read GOAL_AUTHORITY.md and follow the goal-change flow before updating state.'
|
|
90
|
+
: 'Use it as current group goal and role context. The group admin owns charter and dashboard changes unless they explicitly delegate them.';
|
|
91
|
+
return promptBlock(`
|
|
92
|
+
[conversation_goal]
|
|
93
|
+
Current charter for this conversation:
|
|
94
|
+
${charter}
|
|
95
|
+
|
|
96
|
+
${authorityLine}
|
|
97
|
+
[/conversation_goal]
|
|
98
|
+
`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function buildGoalStateBlock(msg) {
|
|
102
|
+
if ((msg.conversation_charter || '').trim()) return '';
|
|
103
|
+
const convType = msg.conversation_type || 'dm';
|
|
104
|
+
const subject = convType === 'group' ? 'This group' : 'This conversation';
|
|
105
|
+
const authorityLine = hasGoalAuthority(msg)
|
|
106
|
+
? 'If this message may be starting, clarifying, or changing an ongoing goal, read GOAL_AUTHORITY.md "Goal Setup When No Specific Goal Exists" and follow it to propose/confirm the goal before writing charter/dashboard state. If it is clearly one-off, answer normally.'
|
|
107
|
+
: 'The group admin owns goal setup; follow direct mentions or assigned tasks normally.';
|
|
108
|
+
return promptBlock(`
|
|
109
|
+
[conversation_goal]
|
|
110
|
+
${subject} does not have a chartered goal yet.
|
|
111
|
+
${authorityLine}
|
|
112
|
+
[/conversation_goal]
|
|
113
|
+
`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function hasGoalAuthority(msg) {
|
|
117
|
+
const convType = msg.conversation_type || 'dm';
|
|
118
|
+
if (convType !== 'group') return true;
|
|
119
|
+
if (msg.recipient_is_conversation_admin === true) return true;
|
|
120
|
+
const recipientRole = String(msg.recipient_conversation_role || msg.recipient_role || '').trim().toLowerCase();
|
|
121
|
+
return recipientRole === 'admin' || recipientRole === 'owner';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function buildQuoteBlock(msg, target = '') {
|
|
125
|
+
const meta = msg.message_metadata || msg.metadata || null;
|
|
126
|
+
const quote = meta && typeof meta === 'object' ? meta.quote : null;
|
|
127
|
+
if (!quote || typeof quote !== 'object') return '';
|
|
128
|
+
const kind = String(quote.kind || '').trim();
|
|
129
|
+
const ref = String(quote.ref || '').trim();
|
|
130
|
+
const snippet = String(quote.snippet || '').trim();
|
|
131
|
+
if (!kind || !ref) return '';
|
|
132
|
+
const fetchHint = kind === 'briefing'
|
|
133
|
+
? `ticlawk briefing get ${ref}`
|
|
134
|
+
: kind === 'dashboard'
|
|
135
|
+
? `ticlawk dashboard get --conversation-id ${ref}`
|
|
136
|
+
: kind === 'message'
|
|
137
|
+
? `ticlawk message read --target ${JSON.stringify(target)} --around ${ref}`
|
|
138
|
+
: '';
|
|
139
|
+
const lines = [
|
|
140
|
+
`The incoming message is a reply to a ${kind}.`,
|
|
141
|
+
`Referenced ${kind}: \`${ref}\``,
|
|
142
|
+
];
|
|
143
|
+
if (snippet) lines.push(`Quoted snippet: ${snippet}`);
|
|
144
|
+
if (fetchHint) lines.push(`Fetch it if needed: \`${fetchHint}\``);
|
|
145
|
+
return lines.join('\n');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function senderDescription(msg) {
|
|
149
|
+
const type = msg.sender_type === 'agent' ? 'an agent' : 'a human user';
|
|
150
|
+
const sender = msg.sender_display_name || msg.sender_user_id || msg.sender_agent_id || '';
|
|
151
|
+
return sender ? `@${sender}, ${type}` : type;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function senderFactDescription(msg) {
|
|
155
|
+
const type = msg.sender_type === 'agent' ? 'agent' : 'human user';
|
|
156
|
+
const sender = msg.sender_display_name || msg.sender_user_id || msg.sender_agent_id || '';
|
|
157
|
+
return sender ? `@${sender}, ${type}` : type;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function conversationLabel(msg, target) {
|
|
161
|
+
const convType = msg.conversation_type || 'dm';
|
|
162
|
+
if (convType === 'dm') return 'a one-on-one conversation';
|
|
163
|
+
return target || 'this group';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function buildMessageSummary(msg, target) {
|
|
167
|
+
const convType = msg.conversation_type || 'dm';
|
|
168
|
+
const reason = normalizeDeliveryReasonForPrompt(msg.reason || '');
|
|
169
|
+
const sender = senderDescription(msg);
|
|
170
|
+
const senderFact = senderFactDescription(msg);
|
|
171
|
+
const where = conversationLabel(msg, target);
|
|
172
|
+
|
|
173
|
+
if (convType === 'dm') {
|
|
174
|
+
return `This is a one-on-one message from ${sender}.`;
|
|
175
|
+
}
|
|
176
|
+
if (reason === 'assignment') {
|
|
177
|
+
return `This message assigns or routes work to you in ${where}.\nSender: ${senderFact}`;
|
|
178
|
+
}
|
|
179
|
+
if (reason === 'ambient') {
|
|
180
|
+
return `Sender: ${senderFact}`;
|
|
181
|
+
}
|
|
182
|
+
if (reason === 'mention') {
|
|
183
|
+
return `You were mentioned in ${where} by ${sender}.`;
|
|
184
|
+
}
|
|
185
|
+
if (reason === 'reply_follow') {
|
|
186
|
+
return `This is a reply in ${where} from ${sender}.`;
|
|
187
|
+
}
|
|
188
|
+
if (reason === 'manual') {
|
|
189
|
+
return `This is a manual wake-up or reminder for ${where}.\nSource: ${senderFact}`;
|
|
190
|
+
}
|
|
191
|
+
return `This is a message in ${where} from ${sender}.`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function buildTaskDetailsBlock(msg) {
|
|
195
|
+
if (msg.task_number == null) return '';
|
|
196
|
+
const lines = [`Task #${msg.task_number} is currently \`${msg.task_status || 'todo'}\`.`];
|
|
197
|
+
if (msg.task_title) lines.push(`Task title: ${msg.task_title}`);
|
|
198
|
+
if (msg.task_assignee_agent_id || msg.task_assignee_user_id) {
|
|
199
|
+
const t = msg.task_assignee_type || 'agent';
|
|
200
|
+
const id = msg.task_assignee_agent_id || msg.task_assignee_user_id;
|
|
201
|
+
lines.push(`Assignee: ${t} \`${id}\``);
|
|
202
|
+
}
|
|
203
|
+
return lines.join('\n');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function buildReactionsBlock(msg) {
|
|
207
|
+
const entries = Array.isArray(msg.reactions_summary) ? msg.reactions_summary : [];
|
|
208
|
+
if (entries.length === 0) return '';
|
|
209
|
+
return `Recent reactions: ${entries.join('; ')}`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function buildContentBlock(rawText) {
|
|
213
|
+
const text = String(rawText || '').trim();
|
|
214
|
+
if (!text) return '';
|
|
215
|
+
return promptBlock(`
|
|
216
|
+
Content:
|
|
217
|
+
${text}
|
|
218
|
+
`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Wrap each per-turn message with the concrete dynamic context for this
|
|
222
|
+
// delivery. Durable communication rules live in COMMUNICATION.md; this block
|
|
223
|
+
// only carries the current message and its exact reply target.
|
|
224
|
+
export function buildWakePromptText({ messageSummary, target, rawText, groupContext, charterBlock, goalStateBlock, quoteBlock, taskDetails, reactionsBlock }) {
|
|
225
|
+
const contextPrefix = [charterBlock, goalStateBlock, quoteBlock].filter(Boolean).join('\n\n');
|
|
226
|
+
const prefix = contextPrefix ? `${contextPrefix}\n\n` : '';
|
|
227
|
+
const detailBlocks = [
|
|
228
|
+
messageSummary,
|
|
229
|
+
`Reply target: \`${target}\``,
|
|
230
|
+
groupContext,
|
|
231
|
+
taskDetails ? `Task details:\n${taskDetails}` : '',
|
|
232
|
+
reactionsBlock,
|
|
233
|
+
buildContentBlock(rawText),
|
|
234
|
+
].filter(Boolean).join('\n\n');
|
|
235
|
+
return promptBlock(`
|
|
236
|
+
${prefix}New message received:
|
|
237
|
+
|
|
238
|
+
${detailBlocks}
|
|
239
|
+
`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function buildInboundWakePrompt(msg) {
|
|
243
|
+
const rawText = msg.text || '';
|
|
244
|
+
const baseHeader = buildDebugEnvelopeHeader(msg);
|
|
245
|
+
const header = baseHeader + buildDebugTaskSuffix(msg) + buildDebugReactionsSuffix(msg);
|
|
246
|
+
const target = buildEnvelopeTarget(msg);
|
|
247
|
+
const groupContext = buildGroupContextBlock(msg);
|
|
248
|
+
const charterBlock = buildCharterBlock(msg);
|
|
249
|
+
const goalStateBlock = buildGoalStateBlock(msg);
|
|
250
|
+
const quoteBlock = buildQuoteBlock(msg, target);
|
|
251
|
+
const messageSummary = buildMessageSummary(msg, target);
|
|
252
|
+
const taskDetails = buildTaskDetailsBlock(msg);
|
|
253
|
+
const reactionsBlock = buildReactionsBlock(msg);
|
|
254
|
+
const text = buildWakePromptText({ messageSummary, target, rawText, groupContext, charterBlock, goalStateBlock, quoteBlock, taskDetails, reactionsBlock });
|
|
255
|
+
return {
|
|
256
|
+
header,
|
|
257
|
+
target,
|
|
258
|
+
text,
|
|
259
|
+
rawText,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
@@ -23,12 +23,14 @@ import {
|
|
|
23
23
|
} from './session.mjs';
|
|
24
24
|
import { discoverSessions } from './transcripts.mjs';
|
|
25
25
|
import { buildImageMessageFromInbound } from '../../core/media/inbound.mjs';
|
|
26
|
-
import {
|
|
26
|
+
import { emitWorkerEventBestEffort } from '../../core/events/worker-events.mjs';
|
|
27
27
|
import {
|
|
28
28
|
shouldStreamRuntime,
|
|
29
29
|
reportSubprocessFailure,
|
|
30
30
|
terminalRuntimeFailure,
|
|
31
31
|
updateBindingRuntimeMeta,
|
|
32
|
+
resolveRuntimeSessionScope,
|
|
33
|
+
buildRuntimeSessionMetaPatch,
|
|
32
34
|
} from '../../core/runtime-support.mjs';
|
|
33
35
|
|
|
34
36
|
export const claudeCodeRuntime = {
|
|
@@ -130,31 +132,35 @@ export const claudeCodeRuntime = {
|
|
|
130
132
|
? await buildImageMessageFromInbound(inbound, 'claude-code')
|
|
131
133
|
: inbound.text;
|
|
132
134
|
|
|
133
|
-
// shouldRotate=true means
|
|
135
|
+
// shouldRotate=true means this conversation has no runtime session
|
|
136
|
+
// yet, or the agent was reset and all scoped sessions are invalid.
|
|
134
137
|
// We pass sessionId=null so `claude` creates a fresh session; the new
|
|
135
138
|
// session_id is captured from stream events and persisted below.
|
|
136
139
|
// Unifying rotate + non-rotate into one path means the standing prompt
|
|
137
140
|
// is always attached, so the agent uses the CLI to reply on every
|
|
138
141
|
// turn — including the first.
|
|
139
|
-
const
|
|
140
|
-
const targetSessionId = shouldRotate ? null :
|
|
141
|
-
const errEventSessionId =
|
|
142
|
+
const sessionScope = resolveRuntimeSessionScope(meta, inbound);
|
|
143
|
+
const targetSessionId = sessionScope.shouldRotate ? null : sessionScope.sessionId;
|
|
144
|
+
const errEventSessionId = targetSessionId || binding.id;
|
|
142
145
|
|
|
143
146
|
try {
|
|
144
147
|
const claudePath = requireClaudePath(runtimeClaudePath);
|
|
145
148
|
const claudeVersion = getClaudeCodeRuntimeHealth(claudePath).version || meta.claudeVersion || null;
|
|
146
149
|
const agentEnv = buildAgentRuntimeEnv({
|
|
147
150
|
agentId: binding.id,
|
|
148
|
-
sessionId:
|
|
151
|
+
sessionId: targetSessionId,
|
|
149
152
|
hostId: binding.runtime_host_id,
|
|
153
|
+
conversationId: inbound.conversationId,
|
|
154
|
+
messageId: inbound.messageId,
|
|
155
|
+
target: inbound.envelopeTarget,
|
|
150
156
|
});
|
|
151
|
-
const appendSystemPrompt = buildStandingPrompt({ agentId: binding.id });
|
|
157
|
+
const appendSystemPrompt = buildStandingPrompt({ agentId: binding.id, inbound });
|
|
152
158
|
const result = shouldStreamRuntime(this.name, this)
|
|
153
159
|
? await this.runTurnStream({ sessionId: targetSessionId, projectDir, claudePath, agentEnv }, message, {
|
|
154
160
|
appendSystemPrompt,
|
|
155
161
|
onEvent: async (event) => {
|
|
156
162
|
if (event?.type === 'turn.started') {
|
|
157
|
-
|
|
163
|
+
emitWorkerEventBestEffort({
|
|
158
164
|
adapter,
|
|
159
165
|
binding,
|
|
160
166
|
agent: this.name,
|
|
@@ -167,34 +173,19 @@ export const claudeCodeRuntime = {
|
|
|
167
173
|
},
|
|
168
174
|
logger: ctx.logger,
|
|
169
175
|
});
|
|
170
|
-
} else if (event?.type === 'message.delta' && event.text) {
|
|
171
|
-
await emitWorkerEvent({
|
|
172
|
-
adapter,
|
|
173
|
-
binding,
|
|
174
|
-
agent: this.name,
|
|
175
|
-
sessionId: event.sessionId || targetSessionId || binding.id,
|
|
176
|
-
cwd: projectDir,
|
|
177
|
-
replyToMessageId: inbound.messageId || null,
|
|
178
|
-
event: {
|
|
179
|
-
hook_event_name: 'worker.message.delta',
|
|
180
|
-
worker_event_name: 'worker.message.delta',
|
|
181
|
-
delta: event.text,
|
|
182
|
-
},
|
|
183
|
-
logger: ctx.logger,
|
|
184
|
-
});
|
|
185
176
|
}
|
|
186
177
|
},
|
|
187
178
|
})
|
|
188
179
|
: await this.runTurn({ sessionId: targetSessionId, projectDir, claudePath, agentEnv }, message, { appendSystemPrompt });
|
|
189
180
|
const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
|
|
190
|
-
|
|
181
|
+
...buildRuntimeSessionMetaPatch(meta, sessionScope, {
|
|
182
|
+
sessionId: result?.sessionId,
|
|
183
|
+
}),
|
|
191
184
|
runtimePath: claudePath,
|
|
192
185
|
claudePath,
|
|
193
186
|
claudeVersion,
|
|
194
|
-
rotatePending: false,
|
|
195
|
-
lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
|
|
196
187
|
}, { status: 'connected' });
|
|
197
|
-
|
|
188
|
+
emitWorkerEventBestEffort({
|
|
198
189
|
adapter,
|
|
199
190
|
binding: nextBinding,
|
|
200
191
|
agent: this.name,
|
|
@@ -209,7 +200,7 @@ export const claudeCodeRuntime = {
|
|
|
209
200
|
});
|
|
210
201
|
return true;
|
|
211
202
|
} catch (err) {
|
|
212
|
-
|
|
203
|
+
emitWorkerEventBestEffort({
|
|
213
204
|
adapter,
|
|
214
205
|
binding,
|
|
215
206
|
agent: this.name,
|
|
@@ -239,24 +230,6 @@ export const claudeCodeRuntime = {
|
|
|
239
230
|
}
|
|
240
231
|
},
|
|
241
232
|
|
|
242
|
-
async reconcileAfterRestart(binding, ctx) {
|
|
243
|
-
const meta = binding.runtimeMeta || {};
|
|
244
|
-
await emitWorkerEvent({
|
|
245
|
-
adapter: ctx.adapter,
|
|
246
|
-
binding,
|
|
247
|
-
agent: this.name,
|
|
248
|
-
sessionId: meta.sessionId || binding.id,
|
|
249
|
-
cwd: ensureAgentHome(binding.id) || '',
|
|
250
|
-
event: {
|
|
251
|
-
hook_event_name: 'Stop',
|
|
252
|
-
worker_event_name: 'worker.turn.complete',
|
|
253
|
-
reason: 'connector.restart.reconcile',
|
|
254
|
-
},
|
|
255
|
-
logger: ctx.logger,
|
|
256
|
-
});
|
|
257
|
-
return 1;
|
|
258
|
-
},
|
|
259
|
-
|
|
260
233
|
// Friendly display name for a CC session (encoded project path →
|
|
261
234
|
// last meaningful segment). Used when materializing a binding.
|
|
262
235
|
formatDisplayName(session) {
|
|
@@ -231,23 +231,19 @@ export function streamCCPrompt({
|
|
|
231
231
|
let seenTurnStart = false;
|
|
232
232
|
let activeSessionId = sessionId || null;
|
|
233
233
|
let finalText = '';
|
|
234
|
-
let eventChain = Promise.resolve();
|
|
235
234
|
|
|
236
235
|
const emit = (event) => {
|
|
237
236
|
if (typeof onEvent !== 'function') return;
|
|
238
|
-
|
|
237
|
+
void Promise.resolve()
|
|
239
238
|
.then(() => onEvent(event))
|
|
240
239
|
.catch(() => {});
|
|
241
|
-
return eventChain;
|
|
242
240
|
};
|
|
243
241
|
|
|
244
242
|
const settle = (fn, value) => {
|
|
245
243
|
if (settled) return;
|
|
246
244
|
settled = true;
|
|
247
245
|
if (timeout) clearTimeout(timeout);
|
|
248
|
-
|
|
249
|
-
.catch(() => {})
|
|
250
|
-
.finally(() => fn(value));
|
|
246
|
+
fn(value);
|
|
251
247
|
};
|
|
252
248
|
|
|
253
249
|
const parseLine = (line) => {
|
|
@@ -267,7 +263,6 @@ export function streamCCPrompt({
|
|
|
267
263
|
const deltaText = parsed.event?.delta?.text;
|
|
268
264
|
if (typeof deltaText === 'string' && deltaText) {
|
|
269
265
|
finalText += deltaText;
|
|
270
|
-
emit({ type: 'message.delta', sessionId: activeSessionId, text: deltaText });
|
|
271
266
|
}
|
|
272
267
|
return;
|
|
273
268
|
}
|
|
@@ -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
|
};
|