@ouro.bot/cli 0.1.0-alpha.15 → 0.1.0-alpha.17
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/dist/heart/config.js +29 -1
- package/dist/heart/core.js +64 -1
- package/dist/heart/daemon/daemon-cli.js +2 -1
- package/dist/heart/streaming.js +55 -1
- package/dist/mind/prompt.js +3 -3
- package/dist/repertoire/ado-client.js +4 -2
- package/dist/repertoire/coding/feedback.js +134 -0
- package/dist/repertoire/coding/index.js +4 -1
- package/dist/repertoire/coding/manager.js +61 -2
- package/dist/repertoire/coding/spawner.js +3 -3
- package/dist/repertoire/coding/tools.js +41 -2
- package/dist/repertoire/data/ado-endpoints.json +188 -0
- package/dist/repertoire/tools-base.js +1 -1
- package/dist/repertoire/tools-teams.js +57 -4
- package/dist/repertoire/tools.js +40 -9
- package/dist/senses/bluebubbles-client.js +159 -5
- package/dist/senses/bluebubbles-media.js +244 -0
- package/dist/senses/bluebubbles.js +108 -19
- package/dist/senses/debug-activity.js +107 -0
- package/dist/senses/teams.js +173 -54
- package/package.json +1 -1
- package/subagents/work-doer.md +26 -24
- package/subagents/work-merger.md +24 -30
- package/subagents/work-planner.md +34 -25
package/dist/heart/config.js
CHANGED
|
@@ -41,8 +41,10 @@ exports.getMinimaxConfig = getMinimaxConfig;
|
|
|
41
41
|
exports.getAnthropicConfig = getAnthropicConfig;
|
|
42
42
|
exports.getOpenAICodexConfig = getOpenAICodexConfig;
|
|
43
43
|
exports.getTeamsConfig = getTeamsConfig;
|
|
44
|
+
exports.getTeamsSecondaryConfig = getTeamsSecondaryConfig;
|
|
44
45
|
exports.getContextConfig = getContextConfig;
|
|
45
46
|
exports.getOAuthConfig = getOAuthConfig;
|
|
47
|
+
exports.resolveOAuthForTenant = resolveOAuthForTenant;
|
|
46
48
|
exports.getTeamsChannelConfig = getTeamsChannelConfig;
|
|
47
49
|
exports.getBlueBubblesConfig = getBlueBubblesConfig;
|
|
48
50
|
exports.getBlueBubblesChannelConfig = getBlueBubblesChannelConfig;
|
|
@@ -84,12 +86,19 @@ const DEFAULT_SECRETS_TEMPLATE = {
|
|
|
84
86
|
clientId: "",
|
|
85
87
|
clientSecret: "",
|
|
86
88
|
tenantId: "",
|
|
89
|
+
managedIdentityClientId: "",
|
|
87
90
|
},
|
|
88
91
|
oauth: {
|
|
89
92
|
graphConnectionName: "graph",
|
|
90
93
|
adoConnectionName: "ado",
|
|
91
94
|
githubConnectionName: "",
|
|
92
95
|
},
|
|
96
|
+
teamsSecondary: {
|
|
97
|
+
clientId: "",
|
|
98
|
+
clientSecret: "",
|
|
99
|
+
tenantId: "",
|
|
100
|
+
managedIdentityClientId: "",
|
|
101
|
+
},
|
|
93
102
|
teamsChannel: {
|
|
94
103
|
skipConfirmation: true,
|
|
95
104
|
port: 3978,
|
|
@@ -118,6 +127,7 @@ function defaultRuntimeConfig() {
|
|
|
118
127
|
"openai-codex": { ...DEFAULT_SECRETS_TEMPLATE.providers["openai-codex"] },
|
|
119
128
|
},
|
|
120
129
|
teams: { ...DEFAULT_SECRETS_TEMPLATE.teams },
|
|
130
|
+
teamsSecondary: { ...DEFAULT_SECRETS_TEMPLATE.teamsSecondary },
|
|
121
131
|
oauth: { ...DEFAULT_SECRETS_TEMPLATE.oauth },
|
|
122
132
|
context: { ...identity_1.DEFAULT_AGENT_CONTEXT },
|
|
123
133
|
teamsChannel: { ...DEFAULT_SECRETS_TEMPLATE.teamsChannel },
|
|
@@ -262,6 +272,10 @@ function getTeamsConfig() {
|
|
|
262
272
|
const config = loadConfig();
|
|
263
273
|
return { ...config.teams };
|
|
264
274
|
}
|
|
275
|
+
function getTeamsSecondaryConfig() {
|
|
276
|
+
const config = loadConfig();
|
|
277
|
+
return { ...config.teamsSecondary };
|
|
278
|
+
}
|
|
265
279
|
function getContextConfig() {
|
|
266
280
|
if (_testContextOverride) {
|
|
267
281
|
return { ..._testContextOverride };
|
|
@@ -282,6 +296,16 @@ function getOAuthConfig() {
|
|
|
282
296
|
const config = loadConfig();
|
|
283
297
|
return { ...config.oauth };
|
|
284
298
|
}
|
|
299
|
+
/** Resolve OAuth connection names for a specific tenant, falling back to defaults. */
|
|
300
|
+
function resolveOAuthForTenant(tenantId) {
|
|
301
|
+
const base = getOAuthConfig();
|
|
302
|
+
const overrides = tenantId ? base.tenantOverrides?.[tenantId] : undefined;
|
|
303
|
+
return {
|
|
304
|
+
graphConnectionName: overrides?.graphConnectionName ?? base.graphConnectionName,
|
|
305
|
+
adoConnectionName: overrides?.adoConnectionName ?? base.adoConnectionName,
|
|
306
|
+
githubConnectionName: overrides?.githubConnectionName ?? base.githubConnectionName,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
285
309
|
function getTeamsChannelConfig() {
|
|
286
310
|
const config = loadConfig();
|
|
287
311
|
const { skipConfirmation, flushIntervalMs, port } = config.teamsChannel;
|
|
@@ -321,7 +345,11 @@ function sanitizeKey(key) {
|
|
|
321
345
|
return key.replace(/[/:]/g, "_");
|
|
322
346
|
}
|
|
323
347
|
function sessionPath(friendId, channel, key) {
|
|
324
|
-
|
|
348
|
+
// On Azure App Service, os.homedir() returns /root which is ephemeral.
|
|
349
|
+
// Use /home (persistent storage) when WEBSITE_SITE_NAME is set.
|
|
350
|
+
/* v8 ignore next -- Azure vs local path branch; environment-specific @preserve */
|
|
351
|
+
const homeBase = process.env.WEBSITE_SITE_NAME ? "/home" : os.homedir();
|
|
352
|
+
const dir = path.join(homeBase, ".agentstate", (0, identity_1.getAgentName)(), "sessions", friendId, channel);
|
|
325
353
|
fs.mkdirSync(dir, { recursive: true });
|
|
326
354
|
return path.join(dir, sanitizeKey(key) + ".json");
|
|
327
355
|
}
|
package/dist/heart/core.js
CHANGED
|
@@ -8,6 +8,7 @@ exports.getProvider = getProvider;
|
|
|
8
8
|
exports.createSummarize = createSummarize;
|
|
9
9
|
exports.getProviderDisplayLabel = getProviderDisplayLabel;
|
|
10
10
|
exports.stripLastToolCalls = stripLastToolCalls;
|
|
11
|
+
exports.repairOrphanedToolCalls = repairOrphanedToolCalls;
|
|
11
12
|
exports.isTransientError = isTransientError;
|
|
12
13
|
exports.classifyTransientError = classifyTransientError;
|
|
13
14
|
exports.runAgent = runAgent;
|
|
@@ -160,6 +161,68 @@ function stripLastToolCalls(messages) {
|
|
|
160
161
|
}
|
|
161
162
|
}
|
|
162
163
|
}
|
|
164
|
+
// Roles that end a tool-result scan. When scanning forward from an assistant
|
|
165
|
+
// message, stop at the next assistant or user message (tool results must be
|
|
166
|
+
// adjacent to their originating assistant message).
|
|
167
|
+
const TOOL_SCAN_BOUNDARY_ROLES = new Set(["assistant", "user"]);
|
|
168
|
+
// Repair orphaned tool_calls and tool results anywhere in the message history.
|
|
169
|
+
// 1. If an assistant message has tool_calls but missing tool results, inject synthetic error results.
|
|
170
|
+
// 2. If a tool result's tool_call_id doesn't match any tool_calls in a preceding assistant message, remove it.
|
|
171
|
+
// This prevents 400 errors from the API after an aborted turn.
|
|
172
|
+
function repairOrphanedToolCalls(messages) {
|
|
173
|
+
// Pass 1: collect all valid tool_call IDs from assistant messages
|
|
174
|
+
const validCallIds = new Set();
|
|
175
|
+
for (const msg of messages) {
|
|
176
|
+
if (msg.role === "assistant") {
|
|
177
|
+
const asst = msg;
|
|
178
|
+
if (asst.tool_calls) {
|
|
179
|
+
for (const tc of asst.tool_calls)
|
|
180
|
+
validCallIds.add(tc.id);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Pass 2: remove orphaned tool results (tool_call_id not in any assistant's tool_calls)
|
|
185
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
186
|
+
if (messages[i].role === "tool") {
|
|
187
|
+
const toolMsg = messages[i];
|
|
188
|
+
if (!validCallIds.has(toolMsg.tool_call_id)) {
|
|
189
|
+
messages.splice(i, 1);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Pass 3: inject synthetic results for tool_calls missing their tool results
|
|
194
|
+
for (let i = 0; i < messages.length; i++) {
|
|
195
|
+
const msg = messages[i];
|
|
196
|
+
if (msg.role !== "assistant")
|
|
197
|
+
continue;
|
|
198
|
+
const asst = msg;
|
|
199
|
+
if (!asst.tool_calls || asst.tool_calls.length === 0)
|
|
200
|
+
continue;
|
|
201
|
+
// Collect tool result IDs that follow this assistant message
|
|
202
|
+
const resultIds = new Set();
|
|
203
|
+
for (let j = i + 1; j < messages.length; j++) {
|
|
204
|
+
const following = messages[j];
|
|
205
|
+
if (following.role === "tool") {
|
|
206
|
+
resultIds.add(following.tool_call_id);
|
|
207
|
+
}
|
|
208
|
+
else if (TOOL_SCAN_BOUNDARY_ROLES.has(following.role)) {
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const missing = asst.tool_calls.filter((tc) => !resultIds.has(tc.id));
|
|
213
|
+
if (missing.length > 0) {
|
|
214
|
+
const syntheticResults = missing.map((tc) => ({
|
|
215
|
+
role: "tool",
|
|
216
|
+
tool_call_id: tc.id,
|
|
217
|
+
content: "error: tool call was interrupted (previous turn timed out or was aborted)",
|
|
218
|
+
}));
|
|
219
|
+
let insertAt = i + 1;
|
|
220
|
+
while (insertAt < messages.length && messages[insertAt].role === "tool")
|
|
221
|
+
insertAt++;
|
|
222
|
+
messages.splice(insertAt, 0, ...syntheticResults);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
163
226
|
// Detect context overflow errors from Azure or MiniMax
|
|
164
227
|
function isContextOverflow(err) {
|
|
165
228
|
if (!(err instanceof Error))
|
|
@@ -278,7 +341,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
278
341
|
}
|
|
279
342
|
catch { /* unsupported */ }
|
|
280
343
|
const toolPreferences = currentContext?.friend?.toolPreferences;
|
|
281
|
-
const baseTools = options?.tools ?? (0, tools_1.getToolsForChannel)(channel ? (0, channel_1.getChannelCapabilities)(channel) : undefined, toolPreferences && Object.keys(toolPreferences).length > 0 ? toolPreferences : undefined);
|
|
344
|
+
const baseTools = options?.tools ?? (0, tools_1.getToolsForChannel)(channel ? (0, channel_1.getChannelCapabilities)(channel) : undefined, toolPreferences && Object.keys(toolPreferences).length > 0 ? toolPreferences : undefined, currentContext);
|
|
282
345
|
// Rebase provider-owned turn state from canonical messages at user-turn start.
|
|
283
346
|
// This prevents stale provider caches from replaying prior-turn context.
|
|
284
347
|
providerRuntime.resetTurnState(messages);
|
|
@@ -571,7 +571,8 @@ function defaultListDiscoveredAgents() {
|
|
|
571
571
|
return discovered.sort((left, right) => left.localeCompare(right));
|
|
572
572
|
}
|
|
573
573
|
async function defaultLinkFriendIdentity(command) {
|
|
574
|
-
const
|
|
574
|
+
const fp = path.join((0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`, "friends");
|
|
575
|
+
const friendStore = new store_file_1.FileFriendStore(fp);
|
|
575
576
|
const current = await friendStore.get(command.friendId);
|
|
576
577
|
if (!current) {
|
|
577
578
|
return `friend not found: ${command.friendId}`;
|
package/dist/heart/streaming.js
CHANGED
|
@@ -77,6 +77,60 @@ class FinalAnswerParser {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
exports.FinalAnswerParser = FinalAnswerParser;
|
|
80
|
+
function toResponsesUserContent(content) {
|
|
81
|
+
if (typeof content === "string") {
|
|
82
|
+
return content;
|
|
83
|
+
}
|
|
84
|
+
if (!Array.isArray(content)) {
|
|
85
|
+
return "";
|
|
86
|
+
}
|
|
87
|
+
const parts = [];
|
|
88
|
+
for (const part of content) {
|
|
89
|
+
if (!part || typeof part !== "object") {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
93
|
+
parts.push({ type: "input_text", text: part.text });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (part.type === "image_url") {
|
|
97
|
+
const imageUrl = typeof part.image_url?.url === "string" ? part.image_url.url : "";
|
|
98
|
+
if (!imageUrl)
|
|
99
|
+
continue;
|
|
100
|
+
parts.push({
|
|
101
|
+
type: "input_image",
|
|
102
|
+
image_url: imageUrl,
|
|
103
|
+
detail: part.image_url?.detail ?? "auto",
|
|
104
|
+
});
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (part.type === "input_audio" &&
|
|
108
|
+
typeof part.input_audio?.data === "string" &&
|
|
109
|
+
(part.input_audio.format === "mp3" || part.input_audio.format === "wav")) {
|
|
110
|
+
parts.push({
|
|
111
|
+
type: "input_audio",
|
|
112
|
+
input_audio: {
|
|
113
|
+
data: part.input_audio.data,
|
|
114
|
+
format: part.input_audio.format,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (part.type === "file") {
|
|
120
|
+
const fileRecord = { type: "input_file" };
|
|
121
|
+
if (typeof part.file?.file_data === "string")
|
|
122
|
+
fileRecord.file_data = part.file.file_data;
|
|
123
|
+
if (typeof part.file?.file_id === "string")
|
|
124
|
+
fileRecord.file_id = part.file.file_id;
|
|
125
|
+
if (typeof part.file?.filename === "string")
|
|
126
|
+
fileRecord.filename = part.file.filename;
|
|
127
|
+
if (typeof part.file?.file_data === "string" || typeof part.file?.file_id === "string") {
|
|
128
|
+
parts.push(fileRecord);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return parts.length > 0 ? parts : "";
|
|
133
|
+
}
|
|
80
134
|
function toResponsesInput(messages) {
|
|
81
135
|
let instructions = "";
|
|
82
136
|
const input = [];
|
|
@@ -90,7 +144,7 @@ function toResponsesInput(messages) {
|
|
|
90
144
|
}
|
|
91
145
|
if (msg.role === "user") {
|
|
92
146
|
const u = msg;
|
|
93
|
-
input.push({ role: "user", content:
|
|
147
|
+
input.push({ role: "user", content: toResponsesUserContent(u.content) });
|
|
94
148
|
continue;
|
|
95
149
|
}
|
|
96
150
|
if (msg.role === "assistant") {
|
package/dist/mind/prompt.js
CHANGED
|
@@ -282,8 +282,8 @@ function dateSection() {
|
|
|
282
282
|
const today = new Date().toISOString().slice(0, 10);
|
|
283
283
|
return `current date: ${today}`;
|
|
284
284
|
}
|
|
285
|
-
function toolsSection(channel, options) {
|
|
286
|
-
const channelTools = (0, tools_1.getToolsForChannel)((0, channel_1.getChannelCapabilities)(channel));
|
|
285
|
+
function toolsSection(channel, options, context) {
|
|
286
|
+
const channelTools = (0, tools_1.getToolsForChannel)((0, channel_1.getChannelCapabilities)(channel), undefined, context);
|
|
287
287
|
const activeTools = (options?.toolChoiceRequired ?? true) ? [...channelTools, tools_1.finalAnswerTool] : channelTools;
|
|
288
288
|
const list = activeTools
|
|
289
289
|
.map((t) => `- ${t.function.name}: ${t.function.description}`)
|
|
@@ -399,7 +399,7 @@ async function buildSystem(channel = "cli", options, context) {
|
|
|
399
399
|
runtimeInfoSection(channel),
|
|
400
400
|
providerSection(),
|
|
401
401
|
dateSection(),
|
|
402
|
-
toolsSection(channel, options),
|
|
402
|
+
toolsSection(channel, options, context),
|
|
403
403
|
skillsSection(),
|
|
404
404
|
taskBoardSection(),
|
|
405
405
|
buildSessionSummary({
|
|
@@ -28,8 +28,10 @@ function resolveContentType(method, path) {
|
|
|
28
28
|
: "application/json";
|
|
29
29
|
}
|
|
30
30
|
// Generic ADO API request. Returns response body as pretty-printed JSON string.
|
|
31
|
-
|
|
31
|
+
// `host` overrides the base URL for non-standard APIs (e.g. "vsapm.dev.azure.com", "vssps.dev.azure.com").
|
|
32
|
+
async function adoRequest(token, method, org, path, body, host) {
|
|
32
33
|
try {
|
|
34
|
+
const base = host ? `https://${host}/${org}` : `${ADO_BASE}/${org}`;
|
|
33
35
|
(0, runtime_1.emitNervesEvent)({
|
|
34
36
|
event: "client.request_start",
|
|
35
37
|
component: "clients",
|
|
@@ -37,7 +39,7 @@ async function adoRequest(token, method, org, path, body) {
|
|
|
37
39
|
meta: { client: "ado", method, org, path },
|
|
38
40
|
});
|
|
39
41
|
const fullPath = ensureApiVersion(path);
|
|
40
|
-
const url = `${
|
|
42
|
+
const url = `${base}${fullPath}`;
|
|
41
43
|
const contentType = resolveContentType(method, path);
|
|
42
44
|
const opts = {
|
|
43
45
|
method,
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatCodingTail = formatCodingTail;
|
|
4
|
+
exports.attachCodingSessionFeedback = attachCodingSessionFeedback;
|
|
5
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
6
|
+
const TERMINAL_UPDATE_KINDS = new Set(["completed", "failed", "killed"]);
|
|
7
|
+
function clip(text, maxLength = 280) {
|
|
8
|
+
const trimmed = text.trim();
|
|
9
|
+
if (trimmed.length <= maxLength)
|
|
10
|
+
return trimmed;
|
|
11
|
+
return `${trimmed.slice(0, maxLength - 3)}...`;
|
|
12
|
+
}
|
|
13
|
+
function isNoiseLine(line) {
|
|
14
|
+
return (/^-+$/.test(line)
|
|
15
|
+
|| /^Reading prompt from stdin/i.test(line)
|
|
16
|
+
|| /^OpenAI Codex v/i.test(line)
|
|
17
|
+
|| /^workdir:/i.test(line)
|
|
18
|
+
|| /^model:/i.test(line)
|
|
19
|
+
|| /^provider:/i.test(line)
|
|
20
|
+
|| /^approval:/i.test(line)
|
|
21
|
+
|| /^sandbox:/i.test(line)
|
|
22
|
+
|| /^reasoning effort:/i.test(line)
|
|
23
|
+
|| /^reasoning summaries:/i.test(line)
|
|
24
|
+
|| /^session id:/i.test(line)
|
|
25
|
+
|| /^mcp startup:/i.test(line)
|
|
26
|
+
|| /^tokens used$/i.test(line)
|
|
27
|
+
|| /^\d{1,3}(,\d{3})*$/.test(line)
|
|
28
|
+
|| /^\d{4}-\d{2}-\d{2}T.*\bWARN\b/.test(line)
|
|
29
|
+
|| line === "user"
|
|
30
|
+
|| line === "codex");
|
|
31
|
+
}
|
|
32
|
+
function lastMeaningfulLine(text) {
|
|
33
|
+
if (!text)
|
|
34
|
+
return null;
|
|
35
|
+
const lines = text
|
|
36
|
+
.split(/\r?\n/)
|
|
37
|
+
.map((line) => line.trim())
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.filter((line) => !isNoiseLine(line));
|
|
40
|
+
if (lines.length === 0)
|
|
41
|
+
return null;
|
|
42
|
+
return clip(lines.at(-1));
|
|
43
|
+
}
|
|
44
|
+
function formatSessionLabel(session) {
|
|
45
|
+
return `${session.runner} ${session.id}`;
|
|
46
|
+
}
|
|
47
|
+
function isSafeProgressSnippet(snippet) {
|
|
48
|
+
const wordCount = snippet.split(/\s+/).filter(Boolean).length;
|
|
49
|
+
return (snippet.length <= 80
|
|
50
|
+
&& wordCount <= 8
|
|
51
|
+
&& !snippet.includes(":")
|
|
52
|
+
&& !snippet.startsWith("**")
|
|
53
|
+
&& !/^Respond with\b/i.test(snippet)
|
|
54
|
+
&& !/^Coding session metadata\b/i.test(snippet)
|
|
55
|
+
&& !/^sessionId\b/i.test(snippet)
|
|
56
|
+
&& !/^taskRef\b/i.test(snippet)
|
|
57
|
+
&& !/^parentAgent\b/i.test(snippet));
|
|
58
|
+
}
|
|
59
|
+
function pickUpdateSnippet(update) {
|
|
60
|
+
return (lastMeaningfulLine(update.text)
|
|
61
|
+
?? lastMeaningfulLine(update.session.stderrTail)
|
|
62
|
+
?? lastMeaningfulLine(update.session.stdoutTail));
|
|
63
|
+
}
|
|
64
|
+
function formatUpdateMessage(update) {
|
|
65
|
+
const label = formatSessionLabel(update.session);
|
|
66
|
+
const snippet = pickUpdateSnippet(update);
|
|
67
|
+
switch (update.kind) {
|
|
68
|
+
case "progress":
|
|
69
|
+
return snippet && isSafeProgressSnippet(snippet) ? `${label}: ${snippet}` : null;
|
|
70
|
+
case "waiting_input":
|
|
71
|
+
return snippet ? `${label} waiting: ${snippet}` : `${label} waiting`;
|
|
72
|
+
case "stalled":
|
|
73
|
+
return snippet ? `${label} stalled: ${snippet}` : `${label} stalled`;
|
|
74
|
+
case "completed":
|
|
75
|
+
return snippet ? `${label} completed: ${snippet}` : `${label} completed`;
|
|
76
|
+
case "failed":
|
|
77
|
+
return snippet ? `${label} failed: ${snippet}` : `${label} failed`;
|
|
78
|
+
case "killed":
|
|
79
|
+
return `${label} killed`;
|
|
80
|
+
case "spawned":
|
|
81
|
+
return `${label} started`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function formatCodingTail(session) {
|
|
85
|
+
const stdout = session.stdoutTail.trim() || "(empty)";
|
|
86
|
+
const stderr = session.stderrTail.trim() || "(empty)";
|
|
87
|
+
return [
|
|
88
|
+
`sessionId: ${session.id}`,
|
|
89
|
+
`runner: ${session.runner}`,
|
|
90
|
+
`status: ${session.status}`,
|
|
91
|
+
`workdir: ${session.workdir}`,
|
|
92
|
+
"",
|
|
93
|
+
"[stdout]",
|
|
94
|
+
stdout,
|
|
95
|
+
"",
|
|
96
|
+
"[stderr]",
|
|
97
|
+
stderr,
|
|
98
|
+
].join("\n");
|
|
99
|
+
}
|
|
100
|
+
function attachCodingSessionFeedback(manager, session, target) {
|
|
101
|
+
let lastMessage = "";
|
|
102
|
+
let closed = false;
|
|
103
|
+
let unsubscribe = () => { };
|
|
104
|
+
const sendMessage = (message) => {
|
|
105
|
+
if (closed || !message || message === lastMessage) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
lastMessage = message;
|
|
109
|
+
void Promise.resolve(target.send(message)).catch((error) => {
|
|
110
|
+
(0, runtime_1.emitNervesEvent)({
|
|
111
|
+
level: "warn",
|
|
112
|
+
component: "repertoire",
|
|
113
|
+
event: "repertoire.coding_feedback_error",
|
|
114
|
+
message: "coding feedback transport failed",
|
|
115
|
+
meta: {
|
|
116
|
+
sessionId: session.id,
|
|
117
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
sendMessage(formatUpdateMessage({ kind: "spawned", session }));
|
|
123
|
+
unsubscribe = manager.subscribe(session.id, async (update) => {
|
|
124
|
+
sendMessage(formatUpdateMessage(update));
|
|
125
|
+
if (TERMINAL_UPDATE_KINDS.has(update.kind)) {
|
|
126
|
+
closed = true;
|
|
127
|
+
unsubscribe();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
return () => {
|
|
131
|
+
closed = true;
|
|
132
|
+
unsubscribe();
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.formatCodingMonitorReport = exports.CodingSessionMonitor = exports.CodingSessionManager = void 0;
|
|
3
|
+
exports.formatCodingTail = exports.attachCodingSessionFeedback = exports.formatCodingMonitorReport = exports.CodingSessionMonitor = exports.CodingSessionManager = void 0;
|
|
4
4
|
exports.getCodingSessionManager = getCodingSessionManager;
|
|
5
5
|
exports.resetCodingSessionManager = resetCodingSessionManager;
|
|
6
6
|
const runtime_1 = require("../../nerves/runtime");
|
|
@@ -34,3 +34,6 @@ var monitor_1 = require("./monitor");
|
|
|
34
34
|
Object.defineProperty(exports, "CodingSessionMonitor", { enumerable: true, get: function () { return monitor_1.CodingSessionMonitor; } });
|
|
35
35
|
var reporter_1 = require("./reporter");
|
|
36
36
|
Object.defineProperty(exports, "formatCodingMonitorReport", { enumerable: true, get: function () { return reporter_1.formatCodingMonitorReport; } });
|
|
37
|
+
var feedback_1 = require("./feedback");
|
|
38
|
+
Object.defineProperty(exports, "attachCodingSessionFeedback", { enumerable: true, get: function () { return feedback_1.attachCodingSessionFeedback; } });
|
|
39
|
+
Object.defineProperty(exports, "formatCodingTail", { enumerable: true, get: function () { return feedback_1.formatCodingTail; } });
|
|
@@ -63,6 +63,8 @@ function isPidAlive(pid) {
|
|
|
63
63
|
function cloneSession(session) {
|
|
64
64
|
return {
|
|
65
65
|
...session,
|
|
66
|
+
stdoutTail: session.stdoutTail,
|
|
67
|
+
stderrTail: session.stderrTail,
|
|
66
68
|
failure: session.failure
|
|
67
69
|
? {
|
|
68
70
|
...session.failure,
|
|
@@ -115,6 +117,7 @@ function defaultFailureDiagnostics(code, signal, command, args, stdoutTail, stde
|
|
|
115
117
|
}
|
|
116
118
|
class CodingSessionManager {
|
|
117
119
|
records = new Map();
|
|
120
|
+
listeners = new Map();
|
|
118
121
|
spawnProcess;
|
|
119
122
|
nowIso;
|
|
120
123
|
maxRestarts;
|
|
@@ -158,6 +161,8 @@ class CodingSessionManager {
|
|
|
158
161
|
scopeFile: normalizedRequest.scopeFile,
|
|
159
162
|
stateFile: normalizedRequest.stateFile,
|
|
160
163
|
status: "spawning",
|
|
164
|
+
stdoutTail: "",
|
|
165
|
+
stderrTail: "",
|
|
161
166
|
pid: null,
|
|
162
167
|
startedAt: now,
|
|
163
168
|
lastActivityAt: now,
|
|
@@ -188,6 +193,7 @@ class CodingSessionManager {
|
|
|
188
193
|
meta: { id, runner: normalizedRequest.runner, pid: session.pid },
|
|
189
194
|
});
|
|
190
195
|
this.persistState();
|
|
196
|
+
this.notifyListeners(id, { kind: "spawned", session: cloneSession(session) });
|
|
191
197
|
return cloneSession(session);
|
|
192
198
|
}
|
|
193
199
|
listSessions() {
|
|
@@ -199,6 +205,20 @@ class CodingSessionManager {
|
|
|
199
205
|
const record = this.records.get(sessionId);
|
|
200
206
|
return record ? cloneSession(record.session) : null;
|
|
201
207
|
}
|
|
208
|
+
subscribe(sessionId, listener) {
|
|
209
|
+
const listeners = this.listeners.get(sessionId) ?? new Set();
|
|
210
|
+
listeners.add(listener);
|
|
211
|
+
this.listeners.set(sessionId, listeners);
|
|
212
|
+
return () => {
|
|
213
|
+
const current = this.listeners.get(sessionId);
|
|
214
|
+
if (!current)
|
|
215
|
+
return;
|
|
216
|
+
current.delete(listener);
|
|
217
|
+
if (current.size === 0) {
|
|
218
|
+
this.listeners.delete(sessionId);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
202
222
|
sendInput(sessionId, input) {
|
|
203
223
|
const record = this.records.get(sessionId);
|
|
204
224
|
if (!record || !record.process) {
|
|
@@ -234,6 +254,7 @@ class CodingSessionManager {
|
|
|
234
254
|
meta: { id: sessionId },
|
|
235
255
|
});
|
|
236
256
|
this.persistState();
|
|
257
|
+
this.notifyListeners(sessionId, { kind: "killed", session: cloneSession(record.session) });
|
|
237
258
|
return { ok: true, message: `killed ${sessionId}` };
|
|
238
259
|
}
|
|
239
260
|
checkStalls(nowMs = Date.now()) {
|
|
@@ -254,6 +275,7 @@ class CodingSessionManager {
|
|
|
254
275
|
message: "coding session stalled",
|
|
255
276
|
meta: { id: record.session.id, elapsedMs: elapsed },
|
|
256
277
|
});
|
|
278
|
+
this.notifyListeners(record.session.id, { kind: "stalled", session: cloneSession(record.session) });
|
|
257
279
|
if (record.request.autoRestartOnStall !== false && record.session.restartCount < this.maxRestarts) {
|
|
258
280
|
this.restartSession(record, "stalled");
|
|
259
281
|
}
|
|
@@ -297,18 +319,23 @@ class CodingSessionManager {
|
|
|
297
319
|
}
|
|
298
320
|
onOutput(record, text, stream) {
|
|
299
321
|
record.session.lastActivityAt = this.nowIso();
|
|
322
|
+
let updateKind = "progress";
|
|
300
323
|
if (stream === "stdout") {
|
|
301
324
|
record.stdoutTail = appendTail(record.stdoutTail, text);
|
|
325
|
+
record.session.stdoutTail = record.stdoutTail;
|
|
302
326
|
}
|
|
303
327
|
else {
|
|
304
328
|
record.stderrTail = appendTail(record.stderrTail, text);
|
|
329
|
+
record.session.stderrTail = record.stderrTail;
|
|
305
330
|
}
|
|
306
331
|
if (text.includes("status: NEEDS_REVIEW") || text.includes("❌ blocked")) {
|
|
307
332
|
record.session.status = "waiting_input";
|
|
333
|
+
updateKind = "waiting_input";
|
|
308
334
|
}
|
|
309
335
|
if (text.includes("✅ all units complete")) {
|
|
310
336
|
record.session.status = "completed";
|
|
311
337
|
record.session.endedAt = this.nowIso();
|
|
338
|
+
updateKind = "completed";
|
|
312
339
|
}
|
|
313
340
|
(0, runtime_1.emitNervesEvent)({
|
|
314
341
|
component: "repertoire",
|
|
@@ -317,6 +344,12 @@ class CodingSessionManager {
|
|
|
317
344
|
meta: { id: record.session.id, status: record.session.status },
|
|
318
345
|
});
|
|
319
346
|
this.persistState();
|
|
347
|
+
this.notifyListeners(record.session.id, {
|
|
348
|
+
kind: updateKind,
|
|
349
|
+
session: cloneSession(record.session),
|
|
350
|
+
stream,
|
|
351
|
+
text,
|
|
352
|
+
});
|
|
320
353
|
}
|
|
321
354
|
onExit(record, code, signal) {
|
|
322
355
|
if (!record.process)
|
|
@@ -334,6 +367,7 @@ class CodingSessionManager {
|
|
|
334
367
|
record.session.status = "completed";
|
|
335
368
|
record.session.endedAt = this.nowIso();
|
|
336
369
|
this.persistState();
|
|
370
|
+
this.notifyListeners(record.session.id, { kind: "completed", session: cloneSession(record.session) });
|
|
337
371
|
return;
|
|
338
372
|
}
|
|
339
373
|
if (record.request.autoRestartOnCrash !== false && record.session.restartCount < this.maxRestarts) {
|
|
@@ -351,6 +385,7 @@ class CodingSessionManager {
|
|
|
351
385
|
meta: { id: record.session.id, code, signal, command: record.command },
|
|
352
386
|
});
|
|
353
387
|
this.persistState();
|
|
388
|
+
this.notifyListeners(record.session.id, { kind: "failed", session: cloneSession(record.session) });
|
|
354
389
|
}
|
|
355
390
|
restartSession(record, reason) {
|
|
356
391
|
const replacement = normalizeSpawnResult(this.spawnProcess(record.request));
|
|
@@ -359,6 +394,8 @@ class CodingSessionManager {
|
|
|
359
394
|
record.args = [...replacement.args];
|
|
360
395
|
record.stdoutTail = "";
|
|
361
396
|
record.stderrTail = "";
|
|
397
|
+
record.session.stdoutTail = "";
|
|
398
|
+
record.session.stderrTail = "";
|
|
362
399
|
record.session.pid = replacement.process.pid ?? null;
|
|
363
400
|
record.session.restartCount += 1;
|
|
364
401
|
record.session.status = "running";
|
|
@@ -375,6 +412,26 @@ class CodingSessionManager {
|
|
|
375
412
|
});
|
|
376
413
|
this.persistState();
|
|
377
414
|
}
|
|
415
|
+
notifyListeners(sessionId, update) {
|
|
416
|
+
const listeners = this.listeners.get(sessionId);
|
|
417
|
+
if (!listeners || listeners.size === 0)
|
|
418
|
+
return;
|
|
419
|
+
for (const listener of listeners) {
|
|
420
|
+
void Promise.resolve(listener(update)).catch((error) => {
|
|
421
|
+
(0, runtime_1.emitNervesEvent)({
|
|
422
|
+
level: "warn",
|
|
423
|
+
component: "repertoire",
|
|
424
|
+
event: "repertoire.coding_feedback_listener_error",
|
|
425
|
+
message: "coding session listener failed",
|
|
426
|
+
meta: {
|
|
427
|
+
sessionId,
|
|
428
|
+
kind: update.kind,
|
|
429
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
378
435
|
loadPersistedState() {
|
|
379
436
|
if (!this.existsSync(this.stateFilePath)) {
|
|
380
437
|
return;
|
|
@@ -433,6 +490,8 @@ class CodingSessionManager {
|
|
|
433
490
|
...session,
|
|
434
491
|
taskRef: session.taskRef ?? normalizedRequest.taskRef,
|
|
435
492
|
failure: session.failure ?? null,
|
|
493
|
+
stdoutTail: session.stdoutTail ?? session.failure?.stdoutTail ?? "",
|
|
494
|
+
stderrTail: session.stderrTail ?? session.failure?.stderrTail ?? "",
|
|
436
495
|
};
|
|
437
496
|
if (typeof normalizedSession.pid === "number") {
|
|
438
497
|
const alive = this.pidAlive(normalizedSession.pid);
|
|
@@ -451,8 +510,8 @@ class CodingSessionManager {
|
|
|
451
510
|
process: null,
|
|
452
511
|
command: normalizedSession.failure?.command ?? "restored",
|
|
453
512
|
args: normalizedSession.failure ? [...normalizedSession.failure.args] : [],
|
|
454
|
-
stdoutTail: normalizedSession.
|
|
455
|
-
stderrTail: normalizedSession.
|
|
513
|
+
stdoutTail: normalizedSession.stdoutTail,
|
|
514
|
+
stderrTail: normalizedSession.stderrTail,
|
|
456
515
|
});
|
|
457
516
|
this.sequence = Math.max(this.sequence, extractSequence(normalizedSession.id));
|
|
458
517
|
}
|
|
@@ -43,11 +43,11 @@ function buildCommandArgs(runner, workdir) {
|
|
|
43
43
|
command: "claude",
|
|
44
44
|
args: [
|
|
45
45
|
"-p",
|
|
46
|
+
"--verbose",
|
|
47
|
+
"--no-session-persistence",
|
|
46
48
|
"--dangerously-skip-permissions",
|
|
47
49
|
"--add-dir",
|
|
48
50
|
workdir,
|
|
49
|
-
"--input-format",
|
|
50
|
-
"stream-json",
|
|
51
51
|
"--output-format",
|
|
52
52
|
"stream-json",
|
|
53
53
|
],
|
|
@@ -91,7 +91,7 @@ function spawnCodingProcess(request, deps = {}) {
|
|
|
91
91
|
cwd: request.workdir,
|
|
92
92
|
stdio: ["pipe", "pipe", "pipe"],
|
|
93
93
|
});
|
|
94
|
-
proc.stdin.
|
|
94
|
+
proc.stdin.end(`${prompt}\n`);
|
|
95
95
|
(0, runtime_1.emitNervesEvent)({
|
|
96
96
|
component: "repertoire",
|
|
97
97
|
event: "repertoire.coding_spawn_end",
|