@controlflow-ai/daemon 0.1.2 → 0.1.3
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 +54 -6
- package/package.json +3 -1
- package/src/agent-avatar.ts +30 -0
- package/src/agent-key.ts +28 -0
- package/src/agent-permissions.ts +359 -0
- package/src/agent-runtime.ts +795 -28
- package/src/agent-workspace.ts +183 -0
- package/src/app.ts +1970 -79
- package/src/args.ts +54 -7
- package/src/cli.ts +873 -14
- package/src/client.ts +472 -10
- package/src/coco.ts +9 -40
- package/src/codex.ts +33 -5
- package/src/config.ts +28 -4
- package/src/console.ts +230 -20
- package/src/daemon-client.ts +116 -3
- package/src/daemon.ts +936 -98
- package/src/db.ts +3128 -122
- package/src/delivery-ws.ts +269 -0
- package/src/format.ts +4 -1
- package/src/lark/cli.ts +3 -3
- package/src/lark/event-router.ts +60 -4
- package/src/lark/inbound-events.ts +156 -3
- package/src/lark/server-integration.ts +659 -111
- package/src/lark/ws-daemon.ts +136 -10
- package/src/local-api.ts +545 -15
- package/src/local-auth.ts +33 -1
- package/src/message-attachments.ts +71 -0
- package/src/messaging-cli.ts +741 -0
- package/src/messaging-status.ts +669 -0
- package/src/migrations/024_agents_model.ts +10 -0
- package/src/migrations/025_room_archive.ts +44 -0
- package/src/migrations/026_project_archive.ts +44 -0
- package/src/migrations/027_agent_permission_profiles.ts +16 -0
- package/src/migrations/028_lark_websocket_restart_state.ts +16 -0
- package/src/migrations/029_held_message_drafts.ts +32 -0
- package/src/migrations/030_agent_room_read_state.ts +25 -0
- package/src/migrations/031_room_tasks.ts +29 -0
- package/src/migrations/032_room_reminders.ts +29 -0
- package/src/migrations/033_room_saved_messages.ts +25 -0
- package/src/migrations/034_agent_activity_events.ts +27 -0
- package/src/migrations/035_agent_avatars.ts +17 -0
- package/src/migrations/036_project_agent_defaults.ts +21 -0
- package/src/migrations/037_message_attachments.ts +36 -0
- package/src/migrations/038_agent_activity_room_scope.ts +64 -0
- package/src/migrations/039_message_attachments_path.ts +34 -0
- package/src/migrations/040_message_attachments_file_schema.ts +80 -0
- package/src/migrations/041_room_system_events.ts +30 -0
- package/src/migrations/042_message_attachment_file_kind.ts +52 -0
- package/src/migrations/043_room_mode_skill_registry.ts +92 -0
- package/src/migrations/044_workflow_runtime.ts +69 -0
- package/src/migrations/045_skill_repository_ownership.ts +64 -0
- package/src/migrations.ts +69 -1
- package/src/neeko.ts +40 -4
- package/src/runtime-env.ts +179 -0
- package/src/runtime-registry.ts +83 -13
- package/src/server.ts +244 -4
- package/src/token-file.ts +13 -6
- package/src/types.ts +362 -0
- package/src/workflow-runtime.ts +275 -0
- package/src/web.ts +0 -904
package/src/agent-runtime.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import type { Message, RoomParticipant, RunAction } from './types.js';
|
|
2
|
-
import {
|
|
1
|
+
import type { AgentActivityKind, AgentRoomSubscription, DeliveryContext, EnabledSkill, Message, RoomMode, RoomParticipant, RoomSavedMessage, RunAction } from './types.js';
|
|
2
|
+
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
3
|
+
import { createWriteStream, existsSync, mkdirSync, type WriteStream } from 'node:fs';
|
|
4
|
+
import { delimiter, join, win32 as pathWin32 } from 'node:path';
|
|
5
|
+
import { homeDir, privatePalCliExecutableName } from './config.js';
|
|
3
6
|
import { sanitizeProviderIds } from './provider-identity.js';
|
|
7
|
+
import { buildRuntimeLaunchContext, runtimeLaunchRoot as permissionRuntimeLaunchRoot, runtimeStateHome as permissionRuntimeStateHome, selectAcpPermissionOption, effectivePermissionProfile, type AgentPermissionProfile, type RuntimeLaunchContext } from './agent-permissions.js';
|
|
4
8
|
|
|
5
9
|
export interface AgentRuntimeRunInput {
|
|
6
10
|
agent: string;
|
|
@@ -9,6 +13,8 @@ export interface AgentRuntimeRunInput {
|
|
|
9
13
|
cwd: string;
|
|
10
14
|
agentHome?: string;
|
|
11
15
|
projectCwd?: string;
|
|
16
|
+
permissionProfile?: AgentPermissionProfile;
|
|
17
|
+
launchContext?: RuntimeLaunchContext;
|
|
12
18
|
projectContext?: {
|
|
13
19
|
id: string;
|
|
14
20
|
name: string;
|
|
@@ -23,15 +29,38 @@ export interface AgentRuntimeRunInput {
|
|
|
23
29
|
localDaemonToken?: string;
|
|
24
30
|
runtimeSessionId?: string | null;
|
|
25
31
|
roomParticipants?: RoomParticipant[];
|
|
32
|
+
roomAgentSubscriptions?: AgentRoomSubscription[];
|
|
33
|
+
roomMode?: RoomMode;
|
|
34
|
+
enabledSkills?: EnabledSkill[];
|
|
26
35
|
recentMessages?: Message[];
|
|
36
|
+
deliveryContext?: DeliveryContext;
|
|
37
|
+
runtimeAttachments?: RuntimeAttachment[];
|
|
27
38
|
dryRun?: boolean;
|
|
28
39
|
privateCliBinDir?: string;
|
|
29
40
|
palCliCommand?: string;
|
|
41
|
+
runtimeEnv?: NodeJS.ProcessEnv;
|
|
30
42
|
signal?: AbortSignal;
|
|
31
43
|
onStart?: (pid: number | null) => Promise<void>;
|
|
44
|
+
onActivity?: (event: AgentRuntimeActivityEvent) => Promise<void>;
|
|
32
45
|
getAction?: () => Promise<RunAction | null>;
|
|
33
46
|
}
|
|
34
47
|
|
|
48
|
+
export interface RuntimeAttachment {
|
|
49
|
+
id: string;
|
|
50
|
+
messageId: number;
|
|
51
|
+
kind: 'image' | 'file';
|
|
52
|
+
mimeType: string;
|
|
53
|
+
filename: string;
|
|
54
|
+
path: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface AgentRuntimeActivityEvent {
|
|
58
|
+
kind: AgentActivityKind;
|
|
59
|
+
title: string;
|
|
60
|
+
detail?: string;
|
|
61
|
+
metadata?: Record<string, unknown>;
|
|
62
|
+
}
|
|
63
|
+
|
|
35
64
|
export function formatRoomParticipantContext(participants: RoomParticipant[] | undefined): string {
|
|
36
65
|
if (!participants || participants.length === 0) {
|
|
37
66
|
return '\nRoom participants: no local participant snapshot is available yet.';
|
|
@@ -39,12 +68,58 @@ export function formatRoomParticipantContext(participants: RoomParticipant[] | u
|
|
|
39
68
|
const lines = participants.slice(0, 50).map((p) => {
|
|
40
69
|
const participant = sanitizeProviderIds(p.participant_id);
|
|
41
70
|
const name = p.display_name ? ` (${sanitizeProviderIds(p.display_name)})` : '';
|
|
42
|
-
|
|
71
|
+
const mentionKey = p.kind === 'agent' ? `, mention_key=${participant}` : '';
|
|
72
|
+
return `- ${p.kind}: ${participant}${name}${mentionKey}, source=${p.source}`;
|
|
43
73
|
});
|
|
44
74
|
const suffix = participants.length > 50 ? `\n- ... ${participants.length - 50} more participants omitted` : '';
|
|
45
75
|
return `\nRoom participants:\n${lines.join('\n')}${suffix}`;
|
|
46
76
|
}
|
|
47
77
|
|
|
78
|
+
export function formatRoomDeliveryPolicyContext(input: {
|
|
79
|
+
agent: string;
|
|
80
|
+
subscriptions?: AgentRoomSubscription[];
|
|
81
|
+
}): string {
|
|
82
|
+
const modes = new Map((input.subscriptions ?? [])
|
|
83
|
+
.filter((subscription) => subscription.channel_id === null)
|
|
84
|
+
.map((subscription) => [subscription.agent, subscription.mode]));
|
|
85
|
+
const currentMode = modes.get(input.agent) ?? 'mentions';
|
|
86
|
+
const strategyLines = [...modes.entries()]
|
|
87
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
88
|
+
.slice(0, 50)
|
|
89
|
+
.map(([agent, mode]) => {
|
|
90
|
+
const current = agent === input.agent ? ' (you)' : '';
|
|
91
|
+
return `- @${sanitizeProviderIds(agent)}${current}: ${mode}`;
|
|
92
|
+
});
|
|
93
|
+
const strategyBlock = strategyLines.length > 0
|
|
94
|
+
? `\nCurrent agent receive strategies in this room:\n${strategyLines.join('\n')}`
|
|
95
|
+
: '\nCurrent agent receive strategies in this room: no subscription snapshot is available; assume agents default to mentions mode unless a direct mention succeeds.';
|
|
96
|
+
return `\nRoom delivery and mention semantics:
|
|
97
|
+
- A direct mention (\`--mention <mention_key>\` or a message addressed to @agent) wakes that agent even when ordinary room messages would not.
|
|
98
|
+
- For agent participants, use the exact \`mention_key\` shown in Room participants.
|
|
99
|
+
- An explicit @all/@_all/所有人 mention is a room-wide handoff: every active room agent that can participate may receive it. Use it only when all agents should inspect the request.
|
|
100
|
+
- A plain room message without a direct recipient is shared context. Agents in \`all\` mode receive it immediately; agents in \`mentions\` mode usually do not wake unless directly mentioned or included by @all.
|
|
101
|
+
- \`periodic\` means pull/batched attention: do not rely on an immediate wakeup from ordinary chatter. Directly mention the agent when you need timely action.
|
|
102
|
+
- \`muted\` or \`off\` means the agent should not be expected to react unless the system explicitly delivers a direct task.
|
|
103
|
+
- Your current receive strategy here is \`${currentMode}\`. When coordinating, decide whether the other agent will naturally see the room message or needs a direct \`--mention <mention_key>\` send.${strategyBlock}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function formatRoomModeAndSkillContext(input: {
|
|
107
|
+
roomMode?: RoomMode;
|
|
108
|
+
enabledSkills?: EnabledSkill[];
|
|
109
|
+
}): string {
|
|
110
|
+
const mode = input.roomMode ?? 'standard';
|
|
111
|
+
const modeDescription = mode === 'idea_development'
|
|
112
|
+
? 'This room is for developing ideas through conversation and .pal docs before implementation handoff.'
|
|
113
|
+
: 'This is an ordinary PAL room. Do not apply special Idea Development workflow unless the user explicitly asks for it.';
|
|
114
|
+
const skills = input.enabledSkills ?? [];
|
|
115
|
+
const skillBlock = skills.length === 0
|
|
116
|
+
? 'Enabled skills: none.'
|
|
117
|
+
: `Enabled skills:\n${skills.map((skill) => (
|
|
118
|
+
`- ${sanitizeProviderIds(skill.key)} (${sanitizeProviderIds(skill.binding_scope)} priority ${skill.binding_priority}): ${sanitizeProviderIds(skill.name)}\n${sanitizeProviderIds(skill.instruction_content)}`
|
|
119
|
+
)).join('\n')}`;
|
|
120
|
+
return `\nRoom mode and skills:\n- Current room mode: ${mode}\n- ${modeDescription}\n${skillBlock}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
48
123
|
export function formatRecentMessageContext(messages: Message[] | undefined, currentMessageId: number): string {
|
|
49
124
|
if (!messages || messages.length === 0) {
|
|
50
125
|
return '\nRecent room history: no local message history is available yet.';
|
|
@@ -55,35 +130,119 @@ export function formatRecentMessageContext(messages: Message[] | undefined, curr
|
|
|
55
130
|
const recipient = message.recipient ? ` to=@${sanitizeProviderIds(message.recipient)}` : '';
|
|
56
131
|
const sender = sanitizeProviderIds(message.sender);
|
|
57
132
|
const content = sanitizeProviderIds(message.content).replace(/\s+/g, ' ').trim().slice(0, 500);
|
|
58
|
-
|
|
133
|
+
const attachments = formatMessageAttachmentSummary(message);
|
|
134
|
+
return `- [${message.id}${marker}] @${sender}${recipient}${parent}: ${content}${attachments}`;
|
|
59
135
|
});
|
|
60
136
|
const omitted = messages.length > 12 ? `\n- ... ${messages.length - 12} older messages omitted` : '';
|
|
61
137
|
return `\nRecent room history:\n${lines.join('\n')}${omitted}`;
|
|
62
138
|
}
|
|
63
139
|
|
|
140
|
+
function formatMessageLines(messages: Message[], currentMessageId: number): string {
|
|
141
|
+
return messages.map((message) => {
|
|
142
|
+
const marker = message.id === currentMessageId ? ' current' : '';
|
|
143
|
+
const parent = message.parent_id === null ? '' : ` parent=${message.parent_id}`;
|
|
144
|
+
const recipient = message.recipient ? ` to=@${sanitizeProviderIds(message.recipient)}` : '';
|
|
145
|
+
const sender = sanitizeProviderIds(message.sender);
|
|
146
|
+
const content = sanitizeProviderIds(message.content).replace(/\s+/g, ' ').trim().slice(0, 500);
|
|
147
|
+
const attachments = formatMessageAttachmentSummary(message);
|
|
148
|
+
return `- [${message.id}${marker}] @${sender}${recipient}${parent}: ${content}${attachments}`;
|
|
149
|
+
}).join('\n');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function formatMessageAttachmentSummary(message: Message): string {
|
|
153
|
+
if (!message.attachments || message.attachments.length === 0) return '';
|
|
154
|
+
const items = message.attachments.map((attachment) => (
|
|
155
|
+
`${attachment.kind} ${sanitizeProviderIds(attachment.filename || attachment.id)} (${attachment.mime_type}, ${attachment.size_bytes} bytes, path=${attachment.path})`
|
|
156
|
+
));
|
|
157
|
+
return ` [attachments: ${items.join('; ')}]`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function formatSavedRoomContext(savedMessages: RoomSavedMessage[] | undefined): string {
|
|
161
|
+
if (!savedMessages || savedMessages.length === 0) return '';
|
|
162
|
+
const lines = savedMessages.slice(0, 10).map((saved) => {
|
|
163
|
+
const id = sanitizeProviderIds(saved.id);
|
|
164
|
+
const sender = sanitizeProviderIds(saved.message_sender);
|
|
165
|
+
const savedBy = sanitizeProviderIds(saved.saved_by);
|
|
166
|
+
const note = saved.note?.trim()
|
|
167
|
+
? ` note="${sanitizeProviderIds(saved.note).replace(/\s+/g, ' ').trim().slice(0, 240)}"`
|
|
168
|
+
: '';
|
|
169
|
+
const content = sanitizeProviderIds(saved.message_content).replace(/\s+/g, ' ').trim().slice(0, 500);
|
|
170
|
+
return `- [saved ${id} msg #${saved.message_id}] saved by @${savedBy}${note}; original @${sender}: ${content}`;
|
|
171
|
+
});
|
|
172
|
+
const omitted = savedMessages.length > 10 ? `\n- ... ${savedMessages.length - 10} older saved messages omitted` : '';
|
|
173
|
+
return `\nSaved room context:\nThese are room messages and notes humans or agents saved as durable collaboration context.\n${lines.join('\n')}${omitted}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function formatRuntimeAttachments(attachments: RuntimeAttachment[] | undefined): string {
|
|
177
|
+
if (!attachments || attachments.length === 0) return '';
|
|
178
|
+
const imageCount = attachments.filter((attachment) => attachment.kind === 'image').length;
|
|
179
|
+
const fileCount = attachments.length - imageCount;
|
|
180
|
+
const lines = attachments.map((attachment) => (
|
|
181
|
+
`- ${attachment.kind} #${sanitizeProviderIds(attachment.id)} from message ${attachment.messageId}: ${sanitizeProviderIds(attachment.filename)}, ${attachment.mimeType}, local path: ${attachment.path}`
|
|
182
|
+
));
|
|
183
|
+
const summary = imageCount === attachments.length
|
|
184
|
+
? `The triggering message includes ${imageCount} image attachment${imageCount === 1 ? '' : 's'}.`
|
|
185
|
+
: `The triggering message includes ${attachments.length} attachment${attachments.length === 1 ? '' : 's'}, including ${imageCount} image attachment${imageCount === 1 ? '' : 's'} and ${fileCount} file attachment${fileCount === 1 ? '' : 's'}.`;
|
|
186
|
+
return `\nMessage attachments:\n${summary}\nThese files are stored under the PAL room directory and are visible to agents participating in this room. Use the listed local paths if you need to inspect them.\n${lines.join('\n')}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function formatDeliveredRoomContext(context: DeliveryContext | undefined, currentMessageId: number, chatId: string): string {
|
|
190
|
+
if (!context) return formatRecentMessageContext(undefined, currentMessageId);
|
|
191
|
+
const header = '\nDelivered room context:\nThis context contains the newest 50 messages since your previous delivery or room-join cursor.';
|
|
192
|
+
const omitted = context.omittedCount > 0
|
|
193
|
+
? `\nThere are ${context.omittedCount} earlier unread/context messages not shown. Use \`pal messages list --chat-id ${chatId} --after ${context.startMessageId} --limit 50\` to inspect them.`
|
|
194
|
+
: '';
|
|
195
|
+
const lines = context.messages.length > 0
|
|
196
|
+
? `\n${formatMessageLines(context.messages, currentMessageId)}`
|
|
197
|
+
: '\nNo delivered room context messages are available.';
|
|
198
|
+
return `${header}${omitted}${lines}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function formatAllMentionOnlyHandoff(content: string, contextLabel: string): string {
|
|
202
|
+
if (!/^\s*@(?:_all|all|所有人)\s*$/iu.test(content)) return '';
|
|
203
|
+
return `\nAll-agents handoff:
|
|
204
|
+
- The current message contains only an @all mention. Treat it as a broadcast trigger for the most recent prior message in ${contextLabel} from the same sender in this chat/topic.
|
|
205
|
+
- If that prior message contains a concrete request, act on it and send a useful reply; do not dismiss this as acknowledgment-only just because the trigger content is only @all.
|
|
206
|
+
- If no prior concrete request is visible, explain that you need the task details.`;
|
|
207
|
+
}
|
|
208
|
+
|
|
64
209
|
export function runtimeCwd(input: AgentRuntimeRunInput): string {
|
|
65
|
-
return input
|
|
210
|
+
return permissionRuntimeStateHome(input);
|
|
66
211
|
}
|
|
67
212
|
|
|
68
213
|
export function runtimeProjectCwd(input: AgentRuntimeRunInput): string {
|
|
69
214
|
return input.projectCwd ?? input.cwd;
|
|
70
215
|
}
|
|
71
216
|
|
|
217
|
+
export function runtimeLaunchRoot(input: AgentRuntimeRunInput): string {
|
|
218
|
+
return permissionRuntimeLaunchRoot(input);
|
|
219
|
+
}
|
|
220
|
+
|
|
72
221
|
export function buildPalPrompt(input: AgentRuntimeRunInput): string {
|
|
73
222
|
const palCli = input.palCliCommand ?? 'pal';
|
|
74
223
|
const thread = input.message.parent_id === null ? '' : `\nTopic parent message id: ${input.message.parent_id}`;
|
|
75
224
|
const recipient = input.message.recipient ? `\nRecipient: @${sanitizeProviderIds(input.message.recipient)}` : '';
|
|
76
225
|
const agentHome = runtimeCwd(input);
|
|
77
226
|
const projectCwd = runtimeProjectCwd(input);
|
|
227
|
+
const launchContext = input.launchContext ?? buildRuntimeLaunchContext(input);
|
|
78
228
|
const chatName = sanitizeProviderIds(input.message.chat_name);
|
|
229
|
+
const chatRef = input.message.chat_id;
|
|
79
230
|
const sender = sanitizeProviderIds(input.message.sender);
|
|
80
231
|
const content = sanitizeProviderIds(input.message.content);
|
|
232
|
+
const freshnessFlags = `--base-message-id ${input.message.id} --hold-if-stale`;
|
|
81
233
|
const projectContext = input.projectContext
|
|
82
234
|
? input.projectContext.accessible
|
|
83
235
|
? `\nProject context:\n- You are working in project: ${sanitizeProviderIds(input.projectContext.name)}\n- Project computer: ${sanitizeProviderIds(input.projectContext.computerName || input.projectContext.computerId)}\n- Project path: ${input.projectContext.rootPath}\n- This agent is running on the project computer and can use the project workspace path above.`
|
|
84
236
|
: `\nProject context:\n- This room belongs to project: ${sanitizeProviderIds(input.projectContext.name)}\n- Project computer: ${sanitizeProviderIds(input.projectContext.computerName || input.projectContext.computerId)}\n- Project path: ${input.projectContext.rootPath}\n- This agent is currently running on computer: ${sanitizeProviderIds(input.projectContext.currentComputerId || 'unknown')}\n- The computers differ, so the project path is temporarily not accessible from this agent run. Do not claim to inspect or change that path unless access is restored.`
|
|
85
237
|
: '';
|
|
86
238
|
|
|
239
|
+
const messageContext = input.deliveryContext
|
|
240
|
+
? formatDeliveredRoomContext(input.deliveryContext, input.message.id, chatRef)
|
|
241
|
+
: formatRecentMessageContext(input.recentMessages, input.message.id);
|
|
242
|
+
const savedRoomContext = input.deliveryContext ? formatSavedRoomContext(input.deliveryContext.savedMessages) : '';
|
|
243
|
+
const runtimeAttachments = formatRuntimeAttachments(input.runtimeAttachments);
|
|
244
|
+
const messageContextLabel = input.deliveryContext ? 'Delivered room context' : 'Recent room history';
|
|
245
|
+
|
|
87
246
|
return `You are ${input.agent}, a long-running PAL coding agent connected to the pal chat server.
|
|
88
247
|
|
|
89
248
|
PAL is a multi-agent chat platform where humans and agents collaborate through rooms and topics.
|
|
@@ -91,14 +250,14 @@ PAL is a multi-agent chat platform where humans and agents collaborate through r
|
|
|
91
250
|
Workspace contract:
|
|
92
251
|
- Your current working directory is your persistent PAL agent home: ${agentHome}
|
|
93
252
|
- Your cwd is for identity, memory, recovery state, scratch files, and durable local notes.
|
|
94
|
-
-
|
|
253
|
+
- The runtime process starts from your persistent PAL agent home: ${launchContext.runtimeRoot}
|
|
95
254
|
- The project workspace for source inspection, code changes, tests, and Git work is: ${projectCwd}
|
|
255
|
+
- Keep durable agent memory in your agent home, not in the project repository.
|
|
96
256
|
${projectContext}
|
|
97
257
|
|
|
98
258
|
Startup recovery:
|
|
99
|
-
- Read MEMORY.md
|
|
259
|
+
- Read ${agentHome}/MEMORY.md first when it exists.
|
|
100
260
|
- Follow its recovery order and read only the referenced state/kb files you need.
|
|
101
|
-
- Keep durable agent memory in your agent home, not in the project repository.
|
|
102
261
|
|
|
103
262
|
A new chat message arrived.
|
|
104
263
|
|
|
@@ -106,7 +265,12 @@ Message id: ${input.message.id}
|
|
|
106
265
|
Chat: #${chatName}${thread}
|
|
107
266
|
Sender: @${sender}${recipient}
|
|
108
267
|
${formatRoomParticipantContext(input.roomParticipants)}
|
|
109
|
-
${
|
|
268
|
+
${formatRoomDeliveryPolicyContext({ agent: input.agent, subscriptions: input.roomAgentSubscriptions })}
|
|
269
|
+
${formatRoomModeAndSkillContext({ roomMode: input.roomMode, enabledSkills: input.enabledSkills })}
|
|
270
|
+
${messageContext}
|
|
271
|
+
${savedRoomContext}
|
|
272
|
+
${runtimeAttachments}
|
|
273
|
+
${formatAllMentionOnlyHandoff(content, messageContextLabel)}
|
|
110
274
|
Content:
|
|
111
275
|
${content}
|
|
112
276
|
|
|
@@ -118,13 +282,37 @@ Communication rules:
|
|
|
118
282
|
- Store PAL handles such as user:pal_user_ab12cd34 in durable memory. Never store provider-native IDs such as Feishu/Lark open_id, chat_id, message_id, or app_id values.
|
|
119
283
|
|
|
120
284
|
Commands:
|
|
121
|
-
- Send a reply to the same chat: ${palCli} send --chat ${
|
|
122
|
-
- Send a message to
|
|
123
|
-
|
|
285
|
+
- Send a reply to the same chat: ${palCli} send --chat-id ${chatRef} ${freshnessFlags} --from ${input.agent} <message>
|
|
286
|
+
- Send a message to one or more agents and trigger their runtimes: ${palCli} send --chat-id ${chatRef} ${freshnessFlags} --from ${input.agent} --mention <mention_key> [--mention <mention_key> ...] <message>
|
|
287
|
+
Use the exact mention_key shown for agent participants above, for example \`--mention 逻辑尖子生\`.
|
|
288
|
+
- Reply in this topic: ${palCli} send --chat-id ${chatRef} --parent ${input.message.parent_id ?? input.message.id} ${freshnessFlags} --from ${input.agent} <message>
|
|
124
289
|
- List rooms: ${palCli} rooms list
|
|
125
|
-
- View room members: ${palCli} rooms members --room ${
|
|
126
|
-
- Read recent chat messages: ${palCli} messages list --
|
|
290
|
+
- View room members: ${palCli} rooms members --room ${chatRef}
|
|
291
|
+
- Read recent chat messages: ${palCli} messages list --chat-id ${chatRef}
|
|
127
292
|
- Show one message: ${palCli} messages show <message-id>
|
|
293
|
+
- List room tasks: ${palCli} tasks list --chat-id ${chatRef}
|
|
294
|
+
- Create room tasks: ${palCli} tasks create --chat-id ${chatRef} --from ${input.agent} <title> [title...]
|
|
295
|
+
- Claim room tasks: ${palCli} tasks claim --chat-id ${chatRef} --tasks <task-number>[,<task-number>]
|
|
296
|
+
- Update a task: ${palCli} tasks status --chat-id ${chatRef} --task <task-number> --status in_review|done|closed
|
|
297
|
+
- Schedule a reminder from this message: ${palCli} reminders schedule --msg-id ${input.message.id} --delay-seconds <seconds> --title <title> [--repeat daily|weekly|every:2h]
|
|
298
|
+
- List scheduled reminders: ${palCli} reminders list --status scheduled
|
|
299
|
+
- Save this message for later: ${palCli} saved save --msg-id ${input.message.id} --note <note>
|
|
300
|
+
- List saved messages: ${palCli} saved list --chat-id ${chatRef}
|
|
301
|
+
- List held drafts: ${palCli} drafts list
|
|
302
|
+
- Revise a held draft after reading intervening messages: ${palCli} drafts revise <draft-id> <revised message>
|
|
303
|
+
- Stay silent on an obsolete held draft: ${palCli} drafts stay-silent <draft-id>
|
|
304
|
+
- Send an unchanged held draft anyway: ${palCli} drafts send-anyway <draft-id>
|
|
305
|
+
|
|
306
|
+
Markdown-safe sending:
|
|
307
|
+
- For Markdown or any message with special shell characters such as quotes, backticks, dollar signs, command substitutions, or multiple lines, pipe a single-quoted heredoc into \`${palCli} send --stdin\`.
|
|
308
|
+
- Use this pattern for ordinary replies: \`cat << 'PAL_MESSAGE' | ${palCli} send --chat-id ${chatRef} ${freshnessFlags} --from ${input.agent} --stdin\`, then the raw Markdown body, then \`PAL_MESSAGE\`.
|
|
309
|
+
- For topic replies, keep the same pattern and include the parent flag: \`cat << 'PAL_MESSAGE' | ${palCli} send --chat-id ${chatRef} --parent ${input.message.parent_id ?? input.message.id} ${freshnessFlags} --from ${input.agent} --stdin\`.
|
|
310
|
+
- This avoids shared temp files entirely; each send process reads its own stdin stream.
|
|
311
|
+
|
|
312
|
+
Freshness rule:
|
|
313
|
+
- Keep the freshness flags on visible replies. If \`${palCli} send\` reports a held draft, the room changed after the context you saw. Inspect the returned intervening messages before doing anything else.
|
|
314
|
+
- Use \`${palCli} drafts revise <draft-id> <revised message>\` if a reply is still useful, \`${palCli} drafts stay-silent <draft-id>\` if the draft is obsolete, and \`${palCli} drafts send-anyway <draft-id>\` only when the unchanged content is still correct.
|
|
315
|
+
- Do not leave held drafts unresolved after deciding.
|
|
128
316
|
|
|
129
317
|
Finish naturally when the task is done. There is no fixed timeout for this run.`;
|
|
130
318
|
}
|
|
@@ -136,6 +324,23 @@ export interface AgentRuntimeRunResult {
|
|
|
136
324
|
runtimeSessionId?: string | null;
|
|
137
325
|
}
|
|
138
326
|
|
|
327
|
+
export interface AgentRuntimeSteerInput {
|
|
328
|
+
agent: string;
|
|
329
|
+
serverUrl: string;
|
|
330
|
+
runId: string;
|
|
331
|
+
sessionId: string;
|
|
332
|
+
runtimeSessionId?: string | null;
|
|
333
|
+
message: Message;
|
|
334
|
+
cwd: string;
|
|
335
|
+
agentHome?: string;
|
|
336
|
+
projectCwd?: string;
|
|
337
|
+
localDaemonUrl?: string;
|
|
338
|
+
localDaemonToken?: string;
|
|
339
|
+
privateCliBinDir?: string;
|
|
340
|
+
palCliCommand?: string;
|
|
341
|
+
runtimeEnv?: NodeJS.ProcessEnv;
|
|
342
|
+
}
|
|
343
|
+
|
|
139
344
|
export type AgentRuntimeProtocol = 'json-stream' | 'acp';
|
|
140
345
|
|
|
141
346
|
export interface AgentRuntimeCapabilities {
|
|
@@ -143,6 +348,7 @@ export interface AgentRuntimeCapabilities {
|
|
|
143
348
|
resume: 'runtime-session-id' | 'adapter-session-id' | 'none';
|
|
144
349
|
busyDeliveryMode: 'queue' | 'direct' | 'notification' | 'none';
|
|
145
350
|
supportsMcp: boolean;
|
|
351
|
+
supportsSteer: boolean;
|
|
146
352
|
}
|
|
147
353
|
|
|
148
354
|
export interface AgentRuntime {
|
|
@@ -156,24 +362,166 @@ export interface AgentRuntime {
|
|
|
156
362
|
readonly command: string;
|
|
157
363
|
/** Return the working directory for the agent process. Defaults to input.agentHome ?? input.cwd. */
|
|
158
364
|
buildCwd?(input: AgentRuntimeRunInput): string;
|
|
365
|
+
/** Return additional environment variables for the agent process. */
|
|
366
|
+
buildEnv?(input: AgentRuntimeRunInput): Record<string, string>;
|
|
159
367
|
/** Parse stdout/stderr from the agent process. Defaults to trimmed stdout + stderr. */
|
|
160
368
|
parseOutput?(input: { stdout: string; stderr: string; input: AgentRuntimeRunInput }): { output: string; runtimeSessionId?: string | null };
|
|
369
|
+
/** Inject a message into an already running runtime session when supported. */
|
|
370
|
+
steerActiveRun?(input: AgentRuntimeSteerInput): Promise<void>;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export interface PrivateCliEnvOptions {
|
|
374
|
+
platform?: NodeJS.Platform;
|
|
375
|
+
pathDelimiter?: string;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function buildPrivateCliEnv(
|
|
379
|
+
privateCliBinDir: string,
|
|
380
|
+
basePath = '',
|
|
381
|
+
options: PrivateCliEnvOptions = {},
|
|
382
|
+
): { PAL_CLI: string; PATH: string } {
|
|
383
|
+
const platform = options.platform ?? process.platform;
|
|
384
|
+
const cliName = privatePalCliExecutableName(platform);
|
|
385
|
+
const palCli = platform === 'win32' ? pathWin32.join(privateCliBinDir, cliName) : join(privateCliBinDir, cliName);
|
|
386
|
+
return {
|
|
387
|
+
PAL_CLI: palCli,
|
|
388
|
+
PATH: `${privateCliBinDir}${options.pathDelimiter ?? delimiter}${basePath}`,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function runtimeDebugEnabled(): boolean {
|
|
393
|
+
return /^(1|true|yes|on)$/i.test(process.env.PAL_RUNTIME_DEBUG ?? '');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function runtimeDebugLogDir(): string {
|
|
397
|
+
return process.env.PAL_RUNTIME_LOG_DIR?.trim() || join(homeDir(), 'runtime-logs');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function safeFileSegment(value: string): string {
|
|
401
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80) || 'unknown';
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function jsonSafe(value: unknown): unknown {
|
|
405
|
+
if (value instanceof Error) return { name: value.name, message: value.message, stack: value.stack };
|
|
406
|
+
return value;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
class RuntimeDebugLogger {
|
|
410
|
+
readonly path: string;
|
|
411
|
+
private stream: WriteStream;
|
|
412
|
+
private closed = false;
|
|
413
|
+
|
|
414
|
+
private constructor(path: string, stream: WriteStream) {
|
|
415
|
+
this.path = path;
|
|
416
|
+
this.stream = stream;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
static create(runtime: AgentRuntime, input: AgentRuntimeRunInput, prompt: string, args: string[], procCwd: string): RuntimeDebugLogger | null {
|
|
420
|
+
if (!runtimeDebugEnabled()) return null;
|
|
421
|
+
const dir = runtimeDebugLogDir();
|
|
422
|
+
mkdirSync(dir, { recursive: true });
|
|
423
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
424
|
+
const file = [
|
|
425
|
+
timestamp,
|
|
426
|
+
safeFileSegment(runtime.name),
|
|
427
|
+
safeFileSegment(input.agent),
|
|
428
|
+
`message-${input.message.id}`,
|
|
429
|
+
].join('_');
|
|
430
|
+
const logger = new RuntimeDebugLogger(join(dir, `${file}.jsonl`), createWriteStream(join(dir, `${file}.jsonl`), { flags: 'a', mode: 0o600 }));
|
|
431
|
+
logger.write('runtime.start', {
|
|
432
|
+
runtime: runtime.name,
|
|
433
|
+
command: runtime.command,
|
|
434
|
+
args,
|
|
435
|
+
cwd: procCwd,
|
|
436
|
+
projectCwd: runtimeProjectCwd(input),
|
|
437
|
+
agent: input.agent,
|
|
438
|
+
chat: input.message.chat_name,
|
|
439
|
+
messageId: input.message.id,
|
|
440
|
+
runtimeSessionId: input.runtimeSessionId ?? null,
|
|
441
|
+
prompt,
|
|
442
|
+
});
|
|
443
|
+
console.log(`[${runtime.name}] debug log=${logger.path}`);
|
|
444
|
+
return logger;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
write(event: string, data: Record<string, unknown> = {}): void {
|
|
448
|
+
if (this.closed) return;
|
|
449
|
+
const safeData = jsonSafe(data);
|
|
450
|
+
const record = safeData && typeof safeData === 'object' ? safeData as Record<string, unknown> : { value: safeData };
|
|
451
|
+
this.stream.write(`${JSON.stringify({ ts: new Date().toISOString(), event, ...record })}\n`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
close(): void {
|
|
455
|
+
if (this.closed) return;
|
|
456
|
+
this.closed = true;
|
|
457
|
+
this.stream.end();
|
|
458
|
+
}
|
|
161
459
|
}
|
|
162
460
|
|
|
163
461
|
function sleep(ms: number): Promise<void> {
|
|
164
462
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
165
463
|
}
|
|
166
464
|
|
|
167
|
-
|
|
465
|
+
export interface ProcessTreeSignalOptions {
|
|
466
|
+
platform?: NodeJS.Platform;
|
|
467
|
+
killPid?: (pid: number, signal?: NodeJS.Signals | number) => void;
|
|
468
|
+
taskkill?: (pid: number, force: boolean) => Promise<void>;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function runTaskkill(pid: number, force: boolean): Promise<void> {
|
|
472
|
+
return new Promise((resolve, reject) => {
|
|
473
|
+
const args = ['/pid', String(pid), '/t'];
|
|
474
|
+
if (force) args.push('/f');
|
|
475
|
+
const taskkill = spawn('taskkill.exe', args, { windowsHide: true });
|
|
476
|
+
let stderr = '';
|
|
477
|
+
taskkill.stderr.on('data', (chunk) => {
|
|
478
|
+
stderr += String(chunk);
|
|
479
|
+
});
|
|
480
|
+
taskkill.once('error', reject);
|
|
481
|
+
taskkill.once('exit', (code) => {
|
|
482
|
+
if (code === 0) {
|
|
483
|
+
resolve();
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
reject(new Error(`taskkill exited ${code}${stderr.trim() ? `: ${stderr.trim()}` : ''}`));
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export async function signalProcessTree(
|
|
492
|
+
pid: number,
|
|
493
|
+
signal: NodeJS.Signals,
|
|
494
|
+
options: ProcessTreeSignalOptions = {},
|
|
495
|
+
): Promise<void> {
|
|
496
|
+
const platform = options.platform ?? process.platform;
|
|
497
|
+
const killPid = options.killPid ?? process.kill;
|
|
498
|
+
if (platform === 'win32') {
|
|
499
|
+
try {
|
|
500
|
+
await (options.taskkill ?? runTaskkill)(pid, signal === 'SIGKILL');
|
|
501
|
+
} catch {
|
|
502
|
+
try { killPid(pid, signal); } catch { /* already exited */ }
|
|
503
|
+
}
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
168
506
|
try {
|
|
169
|
-
|
|
507
|
+
killPid(-pid, signal);
|
|
170
508
|
} catch {
|
|
171
|
-
try {
|
|
509
|
+
try { killPid(pid, signal); } catch { /* already exited */ }
|
|
172
510
|
}
|
|
173
511
|
}
|
|
174
512
|
|
|
513
|
+
function stopNodeProcess(proc: ChildProcessWithoutNullStreams): void {
|
|
514
|
+
if (!proc.pid) return;
|
|
515
|
+
void signalProcessTree(proc.pid, 'SIGTERM');
|
|
516
|
+
setTimeout(() => {
|
|
517
|
+
if (proc.exitCode === null && proc.signalCode === null && proc.pid) {
|
|
518
|
+
void signalProcessTree(proc.pid, 'SIGKILL');
|
|
519
|
+
}
|
|
520
|
+
}, 2000).unref();
|
|
521
|
+
}
|
|
522
|
+
|
|
175
523
|
async function stopProcess(proc: ReturnType<typeof Bun.spawn>): Promise<number | null> {
|
|
176
|
-
|
|
524
|
+
await signalProcessTree(proc.pid, 'SIGTERM');
|
|
177
525
|
const graceful = await Promise.race([
|
|
178
526
|
proc.exited.then((exitCode) => ({ exited: true as const, exitCode })),
|
|
179
527
|
sleep(2000).then(() => ({ exited: false as const })),
|
|
@@ -181,14 +529,376 @@ async function stopProcess(proc: ReturnType<typeof Bun.spawn>): Promise<number |
|
|
|
181
529
|
|
|
182
530
|
if (graceful.exited) return graceful.exitCode;
|
|
183
531
|
|
|
184
|
-
|
|
185
|
-
proc.kill('SIGKILL');
|
|
532
|
+
await signalProcessTree(proc.pid, 'SIGKILL');
|
|
533
|
+
try { proc.kill('SIGKILL'); } catch { /* already exited */ }
|
|
186
534
|
return await Promise.race([
|
|
187
535
|
proc.exited,
|
|
188
536
|
sleep(500).then(() => null),
|
|
189
537
|
]);
|
|
190
538
|
}
|
|
191
539
|
|
|
540
|
+
interface JsonRpcMessage {
|
|
541
|
+
jsonrpc?: string;
|
|
542
|
+
id?: number | string | null;
|
|
543
|
+
method?: string;
|
|
544
|
+
params?: Record<string, unknown>;
|
|
545
|
+
result?: unknown;
|
|
546
|
+
error?: unknown;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function textFromAcpContent(content: unknown): string {
|
|
550
|
+
if (!content || typeof content !== 'object') return '';
|
|
551
|
+
const record = content as Record<string, unknown>;
|
|
552
|
+
if (record.type === 'text' && typeof record.text === 'string') return record.text;
|
|
553
|
+
if (record.type === 'content') return textFromAcpContent(record.content);
|
|
554
|
+
return '';
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function redactRuntimeLog(value: string): string {
|
|
558
|
+
return value
|
|
559
|
+
.replace(/(api[-_]?key|token|secret|authorization|password)(["'\s:=]+)([^\s"']+)/gi, '$1$2<redacted>')
|
|
560
|
+
.replace(/Bearer\s+[^\s"']+/gi, 'Bearer <redacted>')
|
|
561
|
+
.replace(/sk-[A-Za-z0-9_-]+/g, 'sk-<redacted>');
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function tailLines(lines: string[], count = 8): string[] {
|
|
565
|
+
return lines.slice(Math.max(0, lines.length - count)).map((line) => redactRuntimeLog(line));
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function truncateActivityText(value: string, max = 1200): string {
|
|
569
|
+
const compact = redactRuntimeLog(value).replace(/\s+/g, ' ').trim();
|
|
570
|
+
return compact.length > max ? `${compact.slice(0, max - 1)}...` : compact;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function stringField(record: Record<string, unknown>, key: string): string {
|
|
574
|
+
const value = record[key];
|
|
575
|
+
return typeof value === 'string' ? value : '';
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function activityFromRuntimeJson(line: string): AgentRuntimeActivityEvent | null {
|
|
579
|
+
let event: Record<string, unknown>;
|
|
580
|
+
try {
|
|
581
|
+
const parsed = JSON.parse(line) as unknown;
|
|
582
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
|
|
583
|
+
event = parsed as Record<string, unknown>;
|
|
584
|
+
} catch {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const type = stringField(event, 'type') || stringField(event, 'method') || 'runtime.event';
|
|
589
|
+
const item = event.item && typeof event.item === 'object' && !Array.isArray(event.item) ? event.item as Record<string, unknown> : {};
|
|
590
|
+
const itemType = stringField(item, 'type');
|
|
591
|
+
const text = stringField(event, 'text') || stringField(item, 'text') || stringField(event, 'message');
|
|
592
|
+
const command = stringField(event, 'command') || stringField(item, 'command');
|
|
593
|
+
const detail = truncateActivityText(text || command || line);
|
|
594
|
+
const metadata = { runtimeEventType: type, itemType: itemType || undefined };
|
|
595
|
+
|
|
596
|
+
if (type === 'thread.started') return { kind: 'lifecycle', title: 'Runtime thread started', detail: stringField(event, 'thread_id'), metadata };
|
|
597
|
+
if (type.includes('error') || stringField(event, 'level') === 'error') return { kind: 'error', title: 'Runtime error', detail, metadata };
|
|
598
|
+
if (itemType.includes('command') || itemType.includes('tool') || type.includes('exec') || type.includes('tool')) return { kind: 'tool', title: command ? 'Tool call' : 'Using tool', detail, metadata };
|
|
599
|
+
if (itemType === 'agent_message' || type.includes('message') || text) return { kind: 'output', title: 'Output', detail, metadata };
|
|
600
|
+
if (type.includes('completed')) return { kind: 'lifecycle', title: 'Step completed', detail, metadata };
|
|
601
|
+
if (type.includes('started')) return { kind: 'working', title: 'Working', detail, metadata };
|
|
602
|
+
return { kind: 'thinking', title: 'Thinking', detail, metadata };
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async function emitActivity(input: AgentRuntimeRunInput, event: AgentRuntimeActivityEvent): Promise<void> {
|
|
606
|
+
try {
|
|
607
|
+
await input.onActivity?.(event);
|
|
608
|
+
} catch (error) {
|
|
609
|
+
console.warn(`[runtime] activity write failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function commandPath(command: string, env: Record<string, string | undefined>): string | null {
|
|
614
|
+
if (command.includes('/')) return existsSync(command) ? command : null;
|
|
615
|
+
for (const dir of (env.PATH ?? '').split(delimiter)) {
|
|
616
|
+
if (!dir) continue;
|
|
617
|
+
const candidate = join(dir, command);
|
|
618
|
+
if (existsSync(candidate)) return candidate;
|
|
619
|
+
}
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function runtimeEnvSummary(command: string, env: Record<string, string | undefined>): Record<string, unknown> {
|
|
624
|
+
const krb5 = env.KRB5CCNAME;
|
|
625
|
+
const krb5Path = krb5?.replace(/^FILE:/, '');
|
|
626
|
+
const pathParts = (env.PATH ?? '').split(delimiter).filter(Boolean);
|
|
627
|
+
return {
|
|
628
|
+
commandPath: commandPath(command, env) ?? '<not found in PATH>',
|
|
629
|
+
home: env.HOME ? 'set' : 'missing',
|
|
630
|
+
user: env.USER ? 'set' : 'missing',
|
|
631
|
+
krb5ccname: krb5 ? `${krb5}${krb5Path && existsSync(krb5Path) ? ' exists' : ' missing-file'}` : 'missing',
|
|
632
|
+
lang: env.LANG ?? null,
|
|
633
|
+
lcAll: env.LC_ALL ?? null,
|
|
634
|
+
term: env.TERM ?? null,
|
|
635
|
+
httpsProxy: env.HTTPS_PROXY || env.https_proxy ? 'set' : 'missing',
|
|
636
|
+
httpProxy: env.HTTP_PROXY || env.http_proxy ? 'set' : 'missing',
|
|
637
|
+
noProxy: env.NO_PROXY || env.no_proxy ? 'set' : 'missing',
|
|
638
|
+
palCli: env.PAL_CLI ? 'set' : 'missing',
|
|
639
|
+
pathEntries: pathParts.length,
|
|
640
|
+
pathHead: pathParts.slice(0, 5),
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async function runAcpAgentRuntime(
|
|
645
|
+
runtime: AgentRuntime,
|
|
646
|
+
input: AgentRuntimeRunInput,
|
|
647
|
+
prompt: string,
|
|
648
|
+
args: string[],
|
|
649
|
+
procCwd: string,
|
|
650
|
+
env: Record<string, string | undefined>,
|
|
651
|
+
debugLog: RuntimeDebugLogger | null,
|
|
652
|
+
): Promise<AgentRuntimeRunResult> {
|
|
653
|
+
const envSummary = runtimeEnvSummary(runtime.command, env);
|
|
654
|
+
console.log(`[${runtime.name}] acp env ${JSON.stringify(envSummary)}`);
|
|
655
|
+
await emitActivity(input, { kind: 'working', title: 'Starting ACP runtime', detail: `${runtime.command} ${args[0] ?? ''}`.trim() });
|
|
656
|
+
debugLog?.write('process.env_summary', envSummary);
|
|
657
|
+
const proc = spawn(runtime.command, args, {
|
|
658
|
+
cwd: procCwd,
|
|
659
|
+
detached: true,
|
|
660
|
+
env: env as NodeJS.ProcessEnv,
|
|
661
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
662
|
+
});
|
|
663
|
+
console.log(`[${runtime.name}] spawned pid=${proc.pid ?? '-'}`);
|
|
664
|
+
debugLog?.write('process.spawned', { pid: proc.pid ?? null });
|
|
665
|
+
await input.onStart?.(proc.pid ?? null);
|
|
666
|
+
await emitActivity(input, { kind: 'working', title: 'Runtime process started', detail: proc.pid ? `pid ${proc.pid}` : 'pid unavailable' });
|
|
667
|
+
|
|
668
|
+
let nextId = 1;
|
|
669
|
+
const pending = new Map<number, {
|
|
670
|
+
method: string;
|
|
671
|
+
resolve: (value: unknown) => void;
|
|
672
|
+
reject: (error: Error) => void;
|
|
673
|
+
}>();
|
|
674
|
+
const outputChunks: string[] = [];
|
|
675
|
+
const stderrChunks: string[] = [];
|
|
676
|
+
let sessionId = input.runtimeSessionId ?? null;
|
|
677
|
+
let stoppedByAction: RunAction | null = null;
|
|
678
|
+
let completed = false;
|
|
679
|
+
let childExit: { code: number | null; signal: NodeJS.Signals | null } | null = null;
|
|
680
|
+
let childError: string | null = null;
|
|
681
|
+
const rejectPendingRequests = (reason: string): void => {
|
|
682
|
+
if (pending.size === 0) return;
|
|
683
|
+
const diagnostics = {
|
|
684
|
+
reason,
|
|
685
|
+
pending: Array.from(pending.values()).map((request) => request.method),
|
|
686
|
+
childExit,
|
|
687
|
+
childError,
|
|
688
|
+
stdinDestroyed: proc.stdin.destroyed,
|
|
689
|
+
stdoutTail: tailLines(outputChunks),
|
|
690
|
+
stderrTail: tailLines(stderrChunks),
|
|
691
|
+
};
|
|
692
|
+
console.warn(`[${runtime.name}] acp pending requests failed ${JSON.stringify(diagnostics)}`);
|
|
693
|
+
debugLog?.write('acp.pending_failed', diagnostics);
|
|
694
|
+
for (const [id, request] of Array.from(pending.entries())) {
|
|
695
|
+
pending.delete(id);
|
|
696
|
+
request.reject(new Error(`${reason} before ${request.method} response; childExit=${JSON.stringify(childExit)} stderrTail=${JSON.stringify(diagnostics.stderrTail)}`));
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
proc.once('exit', (code, signal) => {
|
|
700
|
+
childExit = { code, signal };
|
|
701
|
+
console.log(`[${runtime.name}] process exit observed code=${code ?? '-'} signal=${signal ?? '-'}`);
|
|
702
|
+
debugLog?.write('process.exit_observed', childExit);
|
|
703
|
+
rejectPendingRequests('ACP child process exited');
|
|
704
|
+
});
|
|
705
|
+
proc.once('error', (error) => {
|
|
706
|
+
childError = error.message;
|
|
707
|
+
console.warn(`[${runtime.name}] process error ${redactRuntimeLog(error.message)}`);
|
|
708
|
+
debugLog?.write('process.error', { message: redactRuntimeLog(error.message) });
|
|
709
|
+
rejectPendingRequests('ACP child process error');
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
const send = (method: string, params: Record<string, unknown>): Promise<unknown> => {
|
|
713
|
+
const id = nextId;
|
|
714
|
+
nextId += 1;
|
|
715
|
+
const request = { jsonrpc: '2.0', id, method, params };
|
|
716
|
+
const startedAt = Date.now();
|
|
717
|
+
console.log(`[${runtime.name}] acp request id=${id} method=${method}`);
|
|
718
|
+
debugLog?.write('acp.send', request);
|
|
719
|
+
proc.stdin.write(`${JSON.stringify(request)}\n`);
|
|
720
|
+
return new Promise((resolve, reject) => {
|
|
721
|
+
pending.set(id, {
|
|
722
|
+
method,
|
|
723
|
+
resolve: (value) => {
|
|
724
|
+
const elapsedMs = Date.now() - startedAt;
|
|
725
|
+
console.log(`[${runtime.name}] acp response id=${id} method=${method} elapsedMs=${elapsedMs}`);
|
|
726
|
+
resolve(value);
|
|
727
|
+
},
|
|
728
|
+
reject: (error) => {
|
|
729
|
+
const elapsedMs = Date.now() - startedAt;
|
|
730
|
+
console.warn(`[${runtime.name}] acp error id=${id} method=${method} elapsedMs=${elapsedMs} error=${redactRuntimeLog(error.message)}`);
|
|
731
|
+
reject(error);
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
});
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
const reply = (id: number | string, result: unknown): void => {
|
|
738
|
+
const response = { jsonrpc: '2.0', id, result };
|
|
739
|
+
debugLog?.write('acp.client_response', response);
|
|
740
|
+
proc.stdin.write(`${JSON.stringify(response)}\n`);
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
const rejectClientRequest = (id: number | string, method: string): void => {
|
|
744
|
+
const response = { jsonrpc: '2.0', id, error: { code: -32601, message: `Unsupported ACP client method: ${method}` } };
|
|
745
|
+
debugLog?.write('acp.client_response', response);
|
|
746
|
+
proc.stdin.write(`${JSON.stringify(response)}\n`);
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
const handleMessage = (message: JsonRpcMessage): void => {
|
|
750
|
+
if (message.id !== undefined && message.id !== null && !message.method) {
|
|
751
|
+
const pendingRequest = pending.get(Number(message.id));
|
|
752
|
+
if (!pendingRequest) {
|
|
753
|
+
console.warn(`[${runtime.name}] acp response id=${String(message.id)} has no pending request`);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
pending.delete(Number(message.id));
|
|
757
|
+
if (message.error) {
|
|
758
|
+
pendingRequest.reject(new Error(`${pendingRequest.method} failed: ${JSON.stringify(message.error)}`));
|
|
759
|
+
} else {
|
|
760
|
+
pendingRequest.resolve(message.result);
|
|
761
|
+
}
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (message.method === 'session/update') {
|
|
766
|
+
const update = (message.params?.update ?? {}) as Record<string, unknown>;
|
|
767
|
+
if (typeof message.params?.sessionId === 'string') sessionId = message.params.sessionId;
|
|
768
|
+
if (update.sessionUpdate === 'agent_message_chunk') {
|
|
769
|
+
const text = textFromAcpContent(update.content);
|
|
770
|
+
if (text) {
|
|
771
|
+
outputChunks.push(text);
|
|
772
|
+
void emitActivity(input, { kind: 'output', title: 'Output', detail: truncateActivityText(text), metadata: { protocol: 'acp', sessionId } });
|
|
773
|
+
}
|
|
774
|
+
} else if (typeof update.sessionUpdate === 'string') {
|
|
775
|
+
void emitActivity(input, { kind: 'thinking', title: 'Thinking', detail: update.sessionUpdate, metadata: { protocol: 'acp', sessionId } });
|
|
776
|
+
}
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
if (message.id !== undefined && message.id !== null && message.method) {
|
|
781
|
+
if (message.method === 'session/request_permission') {
|
|
782
|
+
reply(message.id, {
|
|
783
|
+
outcome: {
|
|
784
|
+
outcome: 'selected',
|
|
785
|
+
optionId: selectAcpPermissionOption({
|
|
786
|
+
params: message.params,
|
|
787
|
+
profile: effectivePermissionProfile(input),
|
|
788
|
+
context: input.launchContext ?? buildRuntimeLaunchContext(input),
|
|
789
|
+
}),
|
|
790
|
+
},
|
|
791
|
+
});
|
|
792
|
+
} else {
|
|
793
|
+
rejectClientRequest(message.id, message.method);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
const readLines = async (stream: NodeJS.ReadableStream, onLine: (line: string) => void): Promise<void> => {
|
|
799
|
+
let buffer = '';
|
|
800
|
+
for await (const chunk of stream) {
|
|
801
|
+
buffer += String(chunk);
|
|
802
|
+
let newline = buffer.indexOf('\n');
|
|
803
|
+
while (newline >= 0) {
|
|
804
|
+
const line = buffer.slice(0, newline).trim();
|
|
805
|
+
buffer = buffer.slice(newline + 1);
|
|
806
|
+
if (line) onLine(line);
|
|
807
|
+
newline = buffer.indexOf('\n');
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
const rest = buffer.trim();
|
|
811
|
+
if (rest) onLine(rest);
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
const stdoutTask = readLines(proc.stdout, (line) => {
|
|
815
|
+
debugLog?.write('process.stdout', { line });
|
|
816
|
+
try {
|
|
817
|
+
const message = JSON.parse(line) as JsonRpcMessage;
|
|
818
|
+
debugLog?.write('acp.receive', { message });
|
|
819
|
+
handleMessage(message);
|
|
820
|
+
} catch {
|
|
821
|
+
const redacted = redactRuntimeLog(line);
|
|
822
|
+
console.warn(`[${runtime.name}] acp non-json stdout: ${redacted}`);
|
|
823
|
+
outputChunks.push(line);
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
const stderrTask = readLines(proc.stderr, (line) => {
|
|
827
|
+
debugLog?.write('process.stderr', { line });
|
|
828
|
+
stderrChunks.push(line);
|
|
829
|
+
console.warn(`[${runtime.name}] stderr: ${redactRuntimeLog(line)}`);
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
const actionTimer = setInterval(() => {
|
|
833
|
+
void input.getAction?.().then((action) => {
|
|
834
|
+
if (!action || completed || stoppedByAction) return;
|
|
835
|
+
stoppedByAction = action;
|
|
836
|
+
if (sessionId) {
|
|
837
|
+
proc.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method: 'session/cancel', params: { sessionId } })}\n`);
|
|
838
|
+
}
|
|
839
|
+
stopNodeProcess(proc);
|
|
840
|
+
}).catch(() => {});
|
|
841
|
+
}, 1000);
|
|
842
|
+
|
|
843
|
+
try {
|
|
844
|
+
console.log(`[${runtime.name}] acp initialize start`);
|
|
845
|
+
await emitActivity(input, { kind: 'working', title: 'Initializing runtime', detail: 'ACP initialize' });
|
|
846
|
+
const initialized = await send('initialize', {
|
|
847
|
+
protocolVersion: 1,
|
|
848
|
+
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
|
|
849
|
+
clientInfo: { name: 'pal', version: '0.1.0' },
|
|
850
|
+
}) as Record<string, unknown>;
|
|
851
|
+
const capabilities = (initialized.agentCapabilities ?? {}) as Record<string, unknown>;
|
|
852
|
+
const sessionCapabilities = (capabilities.sessionCapabilities ?? {}) as Record<string, unknown>;
|
|
853
|
+
console.log(`[${runtime.name}] acp initialize ok capabilities=${JSON.stringify({ resume: Boolean(sessionCapabilities.resume), loadSession: Boolean(capabilities.loadSession) })}`);
|
|
854
|
+
const sessionParams = { cwd: procCwd, mcpServers: [] };
|
|
855
|
+
let session: Record<string, unknown>;
|
|
856
|
+
if (input.runtimeSessionId && sessionCapabilities.resume) {
|
|
857
|
+
console.log(`[${runtime.name}] acp session/resume start sessionId=${input.runtimeSessionId}`);
|
|
858
|
+
session = await send('session/resume', { sessionId: input.runtimeSessionId, ...sessionParams }) as Record<string, unknown>;
|
|
859
|
+
sessionId = input.runtimeSessionId;
|
|
860
|
+
} else if (input.runtimeSessionId && capabilities.loadSession) {
|
|
861
|
+
console.log(`[${runtime.name}] acp session/load start sessionId=${input.runtimeSessionId}`);
|
|
862
|
+
session = await send('session/load', { sessionId: input.runtimeSessionId, ...sessionParams }) as Record<string, unknown>;
|
|
863
|
+
sessionId = input.runtimeSessionId;
|
|
864
|
+
} else {
|
|
865
|
+
console.log(`[${runtime.name}] acp session/new start cwd=${procCwd}`);
|
|
866
|
+
session = await send('session/new', sessionParams) as Record<string, unknown>;
|
|
867
|
+
if (typeof session.sessionId === 'string') sessionId = session.sessionId;
|
|
868
|
+
}
|
|
869
|
+
if (!sessionId) throw new Error('ACP session did not provide a sessionId');
|
|
870
|
+
console.log(`[${runtime.name}] acp session ready sessionId=${sessionId}`);
|
|
871
|
+
await emitActivity(input, { kind: 'lifecycle', title: 'Runtime session ready', detail: sessionId, metadata: { protocol: 'acp' } });
|
|
872
|
+
|
|
873
|
+
console.log(`[${runtime.name}] acp session/prompt start promptChars=${prompt.length}`);
|
|
874
|
+
await emitActivity(input, { kind: 'working', title: 'Prompt sent', detail: `${prompt.length} chars` });
|
|
875
|
+
await send('session/prompt', {
|
|
876
|
+
sessionId,
|
|
877
|
+
prompt: [{ type: 'text', text: prompt }],
|
|
878
|
+
});
|
|
879
|
+
completed = true;
|
|
880
|
+
console.log(`[${runtime.name}] acp session/prompt completed outputChunks=${outputChunks.length} stderrLines=${stderrChunks.length}`);
|
|
881
|
+
await emitActivity(input, { kind: 'lifecycle', title: 'Runtime completed', detail: `${outputChunks.length} output chunks` });
|
|
882
|
+
} finally {
|
|
883
|
+
clearInterval(actionTimer);
|
|
884
|
+
proc.stdin.end();
|
|
885
|
+
stopNodeProcess(proc);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const exitCode = await Promise.race([
|
|
889
|
+
new Promise<number | null>((resolve) => proc.once('exit', (code) => resolve(code))),
|
|
890
|
+
sleep(3000).then(() => null),
|
|
891
|
+
]);
|
|
892
|
+
debugLog?.write('process.exit', { exitCode, stoppedByAction });
|
|
893
|
+
await Promise.allSettled([stdoutTask, stderrTask]);
|
|
894
|
+
const output = [outputChunks.join(''), stderrChunks.join('\n')].filter(Boolean).join('\n');
|
|
895
|
+
console.log(`[${runtime.name}] output length=${output.length}`);
|
|
896
|
+
const result = { output, runtimeSessionId: sessionId, exitCode: stoppedByAction ? exitCode : 0, stoppedByAction };
|
|
897
|
+
debugLog?.write('runtime.result', result);
|
|
898
|
+
debugLog?.close();
|
|
899
|
+
return result;
|
|
900
|
+
}
|
|
901
|
+
|
|
192
902
|
async function waitForExitOrAction(
|
|
193
903
|
proc: ReturnType<typeof Bun.spawn>,
|
|
194
904
|
input: AgentRuntimeRunInput,
|
|
@@ -233,33 +943,47 @@ export async function runAgentRuntime(
|
|
|
233
943
|
const prompt = runtime.buildPrompt(input);
|
|
234
944
|
const args = runtime.buildArgs(input);
|
|
235
945
|
const procCwd = runtime.buildCwd?.(input) ?? runtimeCwd(input);
|
|
946
|
+
const debugLog = RuntimeDebugLogger.create(runtime, input, prompt, args, procCwd);
|
|
236
947
|
console.log(`[${runtime.name}] run agent=${input.agent} chat=${input.message.chat_name} message=${input.message.id} cwd=${procCwd} projectCwd=${runtimeProjectCwd(input)} dryRun=${input.dryRun} extraArgs=[${input.extraArgs.join(', ')}]`);
|
|
948
|
+
await emitActivity(input, { kind: 'working', title: 'Run accepted', detail: `${runtime.name} runtime starting in ${procCwd}` });
|
|
237
949
|
|
|
238
950
|
if (input.dryRun) {
|
|
239
951
|
console.log(`[${runtime.name}] dry-run mode, skipping spawn`);
|
|
952
|
+
await emitActivity(input, { kind: 'lifecycle', title: 'Dry run', detail: 'Runtime spawn skipped' });
|
|
240
953
|
const [subcommand, ...restArgs] = args;
|
|
241
954
|
const renderedArgs = [
|
|
242
955
|
subcommand,
|
|
243
956
|
...restArgs.map((arg) => JSON.stringify(arg)),
|
|
244
957
|
].filter(Boolean).join(' ');
|
|
245
958
|
await input.onStart?.(null);
|
|
246
|
-
|
|
959
|
+
const result = {
|
|
247
960
|
output: `[dry-run] ${runtime.command}${renderedArgs ? ` ${renderedArgs}` : ''}`,
|
|
248
961
|
exitCode: 0,
|
|
249
962
|
stoppedByAction: null,
|
|
250
963
|
runtimeSessionId: input.runtimeSessionId ?? null,
|
|
251
964
|
};
|
|
965
|
+
debugLog?.write('runtime.result', result);
|
|
966
|
+
debugLog?.close();
|
|
967
|
+
return result;
|
|
252
968
|
}
|
|
253
969
|
|
|
970
|
+
const baseEnv = input.runtimeEnv ?? process.env;
|
|
254
971
|
const env = {
|
|
255
|
-
...
|
|
972
|
+
...baseEnv,
|
|
256
973
|
PAL_SERVER: input.serverUrl,
|
|
257
974
|
PAL_AGENT: input.agent,
|
|
258
975
|
...(input.localDaemonUrl ? { LOCK_DAEMON_URL: input.localDaemonUrl } : {}),
|
|
259
976
|
...(input.localDaemonToken ? { LOCK_DAEMON_TOKEN: input.localDaemonToken } : {}),
|
|
260
|
-
...(input.privateCliBinDir ?
|
|
977
|
+
...(input.privateCliBinDir ? buildPrivateCliEnv(input.privateCliBinDir, baseEnv.PATH ?? '') : {}),
|
|
978
|
+
...(runtime.buildEnv?.(input) ?? {}),
|
|
261
979
|
};
|
|
262
980
|
console.log(`[${runtime.name}] spawning: ${runtime.command} ${args.map((a) => JSON.stringify(a)).join(' ')}`);
|
|
981
|
+
if (runtime.capabilities.protocol === 'acp') {
|
|
982
|
+
return runAcpAgentRuntime(runtime, input, prompt, args, procCwd, env, debugLog);
|
|
983
|
+
}
|
|
984
|
+
const envSummary = runtimeEnvSummary(runtime.command, env);
|
|
985
|
+
console.log(`[${runtime.name}] env ${JSON.stringify(envSummary)}`);
|
|
986
|
+
debugLog?.write('process.env_summary', envSummary);
|
|
263
987
|
const proc = Bun.spawn([runtime.command, ...args], {
|
|
264
988
|
cwd: procCwd,
|
|
265
989
|
stdout: 'pipe',
|
|
@@ -269,14 +993,49 @@ export async function runAgentRuntime(
|
|
|
269
993
|
});
|
|
270
994
|
|
|
271
995
|
console.log(`[${runtime.name}] spawned pid=${proc.pid}`);
|
|
996
|
+
debugLog?.write('process.spawned', { pid: proc.pid });
|
|
272
997
|
await input.onStart?.(proc.pid);
|
|
998
|
+
await emitActivity(input, { kind: 'working', title: 'Runtime process started', detail: `pid ${proc.pid}` });
|
|
999
|
+
|
|
1000
|
+
const stdoutLines: string[] = [];
|
|
1001
|
+
const stderrLines: string[] = [];
|
|
1002
|
+
const collectLines = async (stream: ReadableStream<Uint8Array>, onLine: (line: string) => void): Promise<string> => {
|
|
1003
|
+
const decoder = new TextDecoder();
|
|
1004
|
+
let buffer = '';
|
|
1005
|
+
let text = '';
|
|
1006
|
+
for await (const chunk of stream) {
|
|
1007
|
+
const part = decoder.decode(chunk, { stream: true });
|
|
1008
|
+
text += part;
|
|
1009
|
+
buffer += part;
|
|
1010
|
+
let newline = buffer.indexOf('\n');
|
|
1011
|
+
while (newline >= 0) {
|
|
1012
|
+
const line = buffer.slice(0, newline).trim();
|
|
1013
|
+
buffer = buffer.slice(newline + 1);
|
|
1014
|
+
if (line) onLine(line);
|
|
1015
|
+
newline = buffer.indexOf('\n');
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
const rest = `${buffer}${decoder.decode()}`.trim();
|
|
1019
|
+
if (rest) onLine(rest);
|
|
1020
|
+
return text;
|
|
1021
|
+
};
|
|
1022
|
+
const stdoutTask = collectLines(proc.stdout, (line) => {
|
|
1023
|
+
stdoutLines.push(line);
|
|
1024
|
+
const activity = activityFromRuntimeJson(line);
|
|
1025
|
+
if (activity) void emitActivity(input, activity);
|
|
1026
|
+
});
|
|
1027
|
+
const stderrTask = collectLines(proc.stderr, (line) => {
|
|
1028
|
+
stderrLines.push(line);
|
|
1029
|
+
void emitActivity(input, { kind: 'error', title: 'Runtime stderr', detail: truncateActivityText(line) });
|
|
1030
|
+
});
|
|
1031
|
+
|
|
273
1032
|
const status = await waitForExitOrAction(proc, input);
|
|
274
1033
|
console.log(`[${runtime.name}] process exited exitCode=${status.exitCode} stoppedByAction=${status.stoppedByAction ?? '-'}`);
|
|
1034
|
+
debugLog?.write('process.exit', status);
|
|
275
1035
|
|
|
276
|
-
const [stdout, stderr] = await Promise.all([
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
]);
|
|
1036
|
+
const [stdout, stderr] = await Promise.all([stdoutTask, stderrTask]);
|
|
1037
|
+
debugLog?.write('process.stdout', { text: stdout });
|
|
1038
|
+
debugLog?.write('process.stderr', { text: stderr });
|
|
280
1039
|
|
|
281
1040
|
const parsed = runtime.parseOutput?.({ stdout, stderr, input }) ?? {
|
|
282
1041
|
output: [stdout.trim(), stderr.trim()].filter(Boolean).join('\n'),
|
|
@@ -284,7 +1043,15 @@ export async function runAgentRuntime(
|
|
|
284
1043
|
};
|
|
285
1044
|
const output = parsed.output;
|
|
286
1045
|
console.log(`[${runtime.name}] output length=${output.length}`);
|
|
287
|
-
|
|
1046
|
+
const result = { output, runtimeSessionId: parsed.runtimeSessionId ?? input.runtimeSessionId ?? null, ...status };
|
|
1047
|
+
await emitActivity(input, {
|
|
1048
|
+
kind: status.stoppedByAction ? 'lifecycle' : status.exitCode === 0 ? 'lifecycle' : 'error',
|
|
1049
|
+
title: status.stoppedByAction ? `Run ${status.stoppedByAction}` : status.exitCode === 0 ? 'Run completed' : 'Run failed',
|
|
1050
|
+
detail: `${stdoutLines.length} stdout events · ${stderrLines.length} stderr lines`,
|
|
1051
|
+
});
|
|
1052
|
+
debugLog?.write('runtime.result', result);
|
|
1053
|
+
debugLog?.close();
|
|
1054
|
+
return result;
|
|
288
1055
|
}
|
|
289
1056
|
|
|
290
1057
|
export type RuntimeDriver = AgentRuntime;
|