@ouro.bot/cli 0.1.0-alpha.2 → 0.1.0-alpha.20
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/AdoptionSpecialist.ouro/agent.json +70 -9
- package/AdoptionSpecialist.ouro/psyche/SOUL.md +5 -2
- package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
- package/assets/ouroboros.png +0 -0
- package/dist/heart/config.js +66 -4
- package/dist/heart/core.js +75 -2
- package/dist/heart/daemon/daemon-cli.js +507 -29
- package/dist/heart/daemon/daemon-entry.js +13 -5
- package/dist/heart/daemon/daemon.js +42 -9
- package/dist/heart/daemon/hatch-animation.js +35 -0
- package/dist/heart/daemon/hatch-flow.js +2 -11
- package/dist/heart/daemon/hatch-specialist.js +6 -1
- package/dist/heart/daemon/ouro-bot-wrapper.js +4 -3
- package/dist/heart/daemon/ouro-path-installer.js +177 -0
- package/dist/heart/daemon/ouro-uti.js +11 -2
- package/dist/heart/daemon/process-manager.js +1 -1
- package/dist/heart/daemon/runtime-logging.js +9 -5
- package/dist/heart/daemon/runtime-metadata.js +118 -0
- package/dist/heart/daemon/sense-manager.js +266 -0
- package/dist/heart/daemon/specialist-orchestrator.js +129 -0
- package/dist/heart/daemon/specialist-prompt.js +98 -0
- package/dist/heart/daemon/specialist-tools.js +237 -0
- package/dist/heart/daemon/subagent-installer.js +10 -1
- package/dist/heart/identity.js +77 -1
- package/dist/heart/providers/anthropic.js +19 -2
- package/dist/heart/sense-truth.js +61 -0
- package/dist/heart/streaming.js +99 -21
- package/dist/mind/bundle-manifest.js +58 -0
- package/dist/mind/friends/channel.js +8 -0
- package/dist/mind/friends/types.js +1 -1
- package/dist/mind/prompt.js +77 -3
- package/dist/nerves/cli-logging.js +15 -2
- 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 +69 -5
- package/dist/repertoire/tools-teams.js +57 -4
- package/dist/repertoire/tools.js +44 -11
- package/dist/senses/bluebubbles-client.js +433 -0
- package/dist/senses/bluebubbles-entry.js +11 -0
- package/dist/senses/bluebubbles-media.js +244 -0
- package/dist/senses/bluebubbles-model.js +253 -0
- package/dist/senses/bluebubbles-mutation-log.js +76 -0
- package/dist/senses/bluebubbles.js +421 -0
- package/dist/senses/cli.js +293 -133
- package/dist/senses/debug-activity.js +107 -0
- package/dist/senses/teams.js +173 -54
- package/package.json +11 -4
- package/subagents/work-doer.md +26 -24
- package/subagents/work-merger.md +24 -30
- package/subagents/work-planner.md +34 -25
- package/dist/inner-worker-entry.js +0 -4
package/dist/heart/streaming.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FinalAnswerParser = void 0;
|
|
3
|
+
exports.FinalAnswerStreamer = exports.FinalAnswerParser = void 0;
|
|
4
4
|
exports.toResponsesInput = toResponsesInput;
|
|
5
5
|
exports.toResponsesTools = toResponsesTools;
|
|
6
6
|
exports.streamChatCompletion = streamChatCompletion;
|
|
@@ -77,6 +77,89 @@ class FinalAnswerParser {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
exports.FinalAnswerParser = FinalAnswerParser;
|
|
80
|
+
// Shared helper: wraps FinalAnswerParser with onClearText + onTextChunk wiring.
|
|
81
|
+
// Used by all streaming providers (Chat Completions, Responses API, Anthropic)
|
|
82
|
+
// so the eager-match streaming pattern lives in one place.
|
|
83
|
+
class FinalAnswerStreamer {
|
|
84
|
+
parser = new FinalAnswerParser();
|
|
85
|
+
_detected = false;
|
|
86
|
+
callbacks;
|
|
87
|
+
constructor(callbacks) {
|
|
88
|
+
this.callbacks = callbacks;
|
|
89
|
+
}
|
|
90
|
+
get detected() { return this._detected; }
|
|
91
|
+
get streamed() { return this.parser.active; }
|
|
92
|
+
/** Mark final_answer as detected. Calls onClearText on the callbacks. */
|
|
93
|
+
activate() {
|
|
94
|
+
if (this._detected)
|
|
95
|
+
return;
|
|
96
|
+
this._detected = true;
|
|
97
|
+
this.callbacks.onClearText?.();
|
|
98
|
+
}
|
|
99
|
+
/** Feed an argument delta through the parser. Emits text via onTextChunk. */
|
|
100
|
+
processDelta(delta) {
|
|
101
|
+
if (!this._detected)
|
|
102
|
+
return;
|
|
103
|
+
const text = this.parser.process(delta);
|
|
104
|
+
if (text)
|
|
105
|
+
this.callbacks.onTextChunk(text);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
exports.FinalAnswerStreamer = FinalAnswerStreamer;
|
|
109
|
+
function toResponsesUserContent(content) {
|
|
110
|
+
if (typeof content === "string") {
|
|
111
|
+
return content;
|
|
112
|
+
}
|
|
113
|
+
if (!Array.isArray(content)) {
|
|
114
|
+
return "";
|
|
115
|
+
}
|
|
116
|
+
const parts = [];
|
|
117
|
+
for (const part of content) {
|
|
118
|
+
if (!part || typeof part !== "object") {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
122
|
+
parts.push({ type: "input_text", text: part.text });
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (part.type === "image_url") {
|
|
126
|
+
const imageUrl = typeof part.image_url?.url === "string" ? part.image_url.url : "";
|
|
127
|
+
if (!imageUrl)
|
|
128
|
+
continue;
|
|
129
|
+
parts.push({
|
|
130
|
+
type: "input_image",
|
|
131
|
+
image_url: imageUrl,
|
|
132
|
+
detail: part.image_url?.detail ?? "auto",
|
|
133
|
+
});
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (part.type === "input_audio" &&
|
|
137
|
+
typeof part.input_audio?.data === "string" &&
|
|
138
|
+
(part.input_audio.format === "mp3" || part.input_audio.format === "wav")) {
|
|
139
|
+
parts.push({
|
|
140
|
+
type: "input_audio",
|
|
141
|
+
input_audio: {
|
|
142
|
+
data: part.input_audio.data,
|
|
143
|
+
format: part.input_audio.format,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (part.type === "file") {
|
|
149
|
+
const fileRecord = { type: "input_file" };
|
|
150
|
+
if (typeof part.file?.file_data === "string")
|
|
151
|
+
fileRecord.file_data = part.file.file_data;
|
|
152
|
+
if (typeof part.file?.file_id === "string")
|
|
153
|
+
fileRecord.file_id = part.file.file_id;
|
|
154
|
+
if (typeof part.file?.filename === "string")
|
|
155
|
+
fileRecord.filename = part.file.filename;
|
|
156
|
+
if (typeof part.file?.file_data === "string" || typeof part.file?.file_id === "string") {
|
|
157
|
+
parts.push(fileRecord);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return parts.length > 0 ? parts : "";
|
|
162
|
+
}
|
|
80
163
|
function toResponsesInput(messages) {
|
|
81
164
|
let instructions = "";
|
|
82
165
|
const input = [];
|
|
@@ -90,7 +173,7 @@ function toResponsesInput(messages) {
|
|
|
90
173
|
}
|
|
91
174
|
if (msg.role === "user") {
|
|
92
175
|
const u = msg;
|
|
93
|
-
input.push({ role: "user", content:
|
|
176
|
+
input.push({ role: "user", content: toResponsesUserContent(u.content) });
|
|
94
177
|
continue;
|
|
95
178
|
}
|
|
96
179
|
if (msg.role === "assistant") {
|
|
@@ -106,6 +189,9 @@ function toResponsesInput(messages) {
|
|
|
106
189
|
}
|
|
107
190
|
if (a.tool_calls) {
|
|
108
191
|
for (const tc of a.tool_calls) {
|
|
192
|
+
/* v8 ignore next -- type narrowing: OpenAI SDK only emits function tool_calls @preserve */
|
|
193
|
+
if (tc.type !== "function")
|
|
194
|
+
continue;
|
|
109
195
|
input.push({
|
|
110
196
|
type: "function_call",
|
|
111
197
|
call_id: tc.id,
|
|
@@ -152,8 +238,7 @@ async function streamChatCompletion(client, createParams, callbacks, signal) {
|
|
|
152
238
|
let toolCalls = {};
|
|
153
239
|
let streamStarted = false;
|
|
154
240
|
let usage;
|
|
155
|
-
const
|
|
156
|
-
let finalAnswerDetected = false;
|
|
241
|
+
const answerStreamer = new FinalAnswerStreamer(callbacks);
|
|
157
242
|
// State machine for parsing inline <think> tags (MiniMax pattern)
|
|
158
243
|
let contentBuf = "";
|
|
159
244
|
let inThinkTag = false;
|
|
@@ -271,21 +356,18 @@ async function streamChatCompletion(client, createParams, callbacks, signal) {
|
|
|
271
356
|
// Detect final_answer tool call on first name delta.
|
|
272
357
|
// Only activate streaming if this is the sole tool call (index 0
|
|
273
358
|
// and no other indices seen). Mixed calls are rejected by core.ts.
|
|
274
|
-
if (tc.function.name === "final_answer" && !
|
|
359
|
+
if (tc.function.name === "final_answer" && !answerStreamer.detected
|
|
275
360
|
&& tc.index === 0 && Object.keys(toolCalls).length === 1) {
|
|
276
|
-
|
|
277
|
-
callbacks.onClearText?.();
|
|
361
|
+
answerStreamer.activate();
|
|
278
362
|
}
|
|
279
363
|
}
|
|
280
364
|
if (tc.function?.arguments) {
|
|
281
365
|
toolCalls[tc.index].arguments += tc.function.arguments;
|
|
282
366
|
// Feed final_answer argument deltas to the parser for progressive
|
|
283
367
|
// streaming, but only when it appears to be the sole tool call.
|
|
284
|
-
if (
|
|
368
|
+
if (answerStreamer.detected && toolCalls[tc.index].name === "final_answer"
|
|
285
369
|
&& Object.keys(toolCalls).length === 1) {
|
|
286
|
-
|
|
287
|
-
if (text)
|
|
288
|
-
callbacks.onTextChunk(text);
|
|
370
|
+
answerStreamer.processDelta(tc.function.arguments);
|
|
289
371
|
}
|
|
290
372
|
}
|
|
291
373
|
}
|
|
@@ -299,7 +381,7 @@ async function streamChatCompletion(client, createParams, callbacks, signal) {
|
|
|
299
381
|
toolCalls: Object.values(toolCalls),
|
|
300
382
|
outputItems: [],
|
|
301
383
|
usage,
|
|
302
|
-
finalAnswerStreamed:
|
|
384
|
+
finalAnswerStreamed: answerStreamer.streamed,
|
|
303
385
|
};
|
|
304
386
|
}
|
|
305
387
|
async function streamResponsesApi(client, createParams, callbacks, signal) {
|
|
@@ -317,9 +399,8 @@ async function streamResponsesApi(client, createParams, callbacks, signal) {
|
|
|
317
399
|
const outputItems = [];
|
|
318
400
|
let currentToolCall = null;
|
|
319
401
|
let usage;
|
|
320
|
-
const
|
|
402
|
+
const answerStreamer = new FinalAnswerStreamer(callbacks);
|
|
321
403
|
let functionCallCount = 0;
|
|
322
|
-
let finalAnswerDetected = false;
|
|
323
404
|
for await (const event of response) {
|
|
324
405
|
if (signal?.aborted)
|
|
325
406
|
break;
|
|
@@ -352,8 +433,7 @@ async function streamResponsesApi(client, createParams, callbacks, signal) {
|
|
|
352
433
|
// Only activate when this is the first (and so far only) function call.
|
|
353
434
|
// Mixed calls are rejected by core.ts; no need to stream their args.
|
|
354
435
|
if (String(event.item.name) === "final_answer" && functionCallCount === 1) {
|
|
355
|
-
|
|
356
|
-
callbacks.onClearText?.();
|
|
436
|
+
answerStreamer.activate();
|
|
357
437
|
}
|
|
358
438
|
}
|
|
359
439
|
break;
|
|
@@ -363,11 +443,9 @@ async function streamResponsesApi(client, createParams, callbacks, signal) {
|
|
|
363
443
|
currentToolCall.arguments += event.delta;
|
|
364
444
|
// Feed final_answer argument deltas to the parser for progressive
|
|
365
445
|
// streaming, but only when it appears to be the sole function call.
|
|
366
|
-
if (
|
|
446
|
+
if (answerStreamer.detected && currentToolCall.name === "final_answer"
|
|
367
447
|
&& functionCallCount === 1) {
|
|
368
|
-
|
|
369
|
-
if (text)
|
|
370
|
-
callbacks.onTextChunk(text);
|
|
448
|
+
answerStreamer.processDelta(String(event.delta));
|
|
371
449
|
}
|
|
372
450
|
}
|
|
373
451
|
break;
|
|
@@ -407,6 +485,6 @@ async function streamResponsesApi(client, createParams, callbacks, signal) {
|
|
|
407
485
|
toolCalls,
|
|
408
486
|
outputItems,
|
|
409
487
|
usage,
|
|
410
|
-
finalAnswerStreamed:
|
|
488
|
+
finalAnswerStreamed: answerStreamer.streamed,
|
|
411
489
|
};
|
|
412
490
|
}
|
|
@@ -34,6 +34,10 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.CANONICAL_BUNDLE_MANIFEST = void 0;
|
|
37
|
+
exports.getPackageVersion = getPackageVersion;
|
|
38
|
+
exports.createBundleMeta = createBundleMeta;
|
|
39
|
+
exports.backfillBundleMeta = backfillBundleMeta;
|
|
40
|
+
exports.resetBackfillTracking = resetBackfillTracking;
|
|
37
41
|
exports.isCanonicalBundlePath = isCanonicalBundlePath;
|
|
38
42
|
exports.findNonCanonicalBundlePaths = findNonCanonicalBundlePaths;
|
|
39
43
|
const fs = __importStar(require("fs"));
|
|
@@ -41,6 +45,7 @@ const path = __importStar(require("path"));
|
|
|
41
45
|
const runtime_1 = require("../nerves/runtime");
|
|
42
46
|
exports.CANONICAL_BUNDLE_MANIFEST = [
|
|
43
47
|
{ path: "agent.json", kind: "file" },
|
|
48
|
+
{ path: "bundle-meta.json", kind: "file" },
|
|
44
49
|
{ path: "psyche/SOUL.md", kind: "file" },
|
|
45
50
|
{ path: "psyche/IDENTITY.md", kind: "file" },
|
|
46
51
|
{ path: "psyche/LORE.md", kind: "file" },
|
|
@@ -53,6 +58,59 @@ exports.CANONICAL_BUNDLE_MANIFEST = [
|
|
|
53
58
|
{ path: "senses", kind: "dir" },
|
|
54
59
|
{ path: "senses/teams", kind: "dir" },
|
|
55
60
|
];
|
|
61
|
+
function getPackageVersion() {
|
|
62
|
+
const packageJsonPath = path.resolve(__dirname, "../../package.json");
|
|
63
|
+
const raw = fs.readFileSync(packageJsonPath, "utf-8");
|
|
64
|
+
const parsed = JSON.parse(raw);
|
|
65
|
+
(0, runtime_1.emitNervesEvent)({
|
|
66
|
+
component: "mind",
|
|
67
|
+
event: "mind.package_version_read",
|
|
68
|
+
message: "read package version",
|
|
69
|
+
meta: { version: parsed.version },
|
|
70
|
+
});
|
|
71
|
+
return parsed.version;
|
|
72
|
+
}
|
|
73
|
+
function createBundleMeta() {
|
|
74
|
+
return {
|
|
75
|
+
runtimeVersion: getPackageVersion(),
|
|
76
|
+
bundleSchemaVersion: 1,
|
|
77
|
+
lastUpdated: new Date().toISOString(),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const _backfilledRoots = new Set();
|
|
81
|
+
/**
|
|
82
|
+
* If bundle-meta.json is missing from the agent root, create it with current runtime version.
|
|
83
|
+
* This backfills existing agent bundles that were created before bundle-meta.json was introduced.
|
|
84
|
+
* Only attempts once per bundleRoot per process.
|
|
85
|
+
*/
|
|
86
|
+
function backfillBundleMeta(bundleRoot) {
|
|
87
|
+
if (_backfilledRoots.has(bundleRoot))
|
|
88
|
+
return;
|
|
89
|
+
_backfilledRoots.add(bundleRoot);
|
|
90
|
+
const metaPath = path.join(bundleRoot, "bundle-meta.json");
|
|
91
|
+
try {
|
|
92
|
+
if (fs.existsSync(metaPath)) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const meta = createBundleMeta();
|
|
96
|
+
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf-8");
|
|
97
|
+
(0, runtime_1.emitNervesEvent)({
|
|
98
|
+
component: "mind",
|
|
99
|
+
event: "mind.bundle_meta_backfill",
|
|
100
|
+
message: "backfilled missing bundle-meta.json",
|
|
101
|
+
meta: { bundleRoot },
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Non-blocking: if we can't write, that's okay
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Reset the backfill tracking set. Used in tests.
|
|
110
|
+
*/
|
|
111
|
+
function resetBackfillTracking() {
|
|
112
|
+
_backfilledRoots.clear();
|
|
113
|
+
}
|
|
56
114
|
const CANONICAL_FILE_PATHS = new Set(exports.CANONICAL_BUNDLE_MANIFEST
|
|
57
115
|
.filter((entry) => entry.kind === "file")
|
|
58
116
|
.map((entry) => entry.path));
|
|
@@ -21,6 +21,14 @@ const CHANNEL_CAPABILITIES = {
|
|
|
21
21
|
supportsRichCards: true,
|
|
22
22
|
maxMessageLength: Infinity,
|
|
23
23
|
},
|
|
24
|
+
bluebubbles: {
|
|
25
|
+
channel: "bluebubbles",
|
|
26
|
+
availableIntegrations: [],
|
|
27
|
+
supportsMarkdown: false,
|
|
28
|
+
supportsStreaming: false,
|
|
29
|
+
supportsRichCards: false,
|
|
30
|
+
maxMessageLength: Infinity,
|
|
31
|
+
},
|
|
24
32
|
};
|
|
25
33
|
const DEFAULT_CAPABILITIES = {
|
|
26
34
|
channel: "cli",
|
|
@@ -5,7 +5,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
5
5
|
exports.isIdentityProvider = isIdentityProvider;
|
|
6
6
|
exports.isIntegration = isIntegration;
|
|
7
7
|
const runtime_1 = require("../../nerves/runtime");
|
|
8
|
-
const IDENTITY_PROVIDERS = new Set(["aad", "local", "teams-conversation"]);
|
|
8
|
+
const IDENTITY_PROVIDERS = new Set(["aad", "local", "teams-conversation", "imessage-handle"]);
|
|
9
9
|
function isIdentityProvider(value) {
|
|
10
10
|
(0, runtime_1.emitNervesEvent)({
|
|
11
11
|
component: "friends",
|
package/dist/mind/prompt.js
CHANGED
|
@@ -47,10 +47,12 @@ const identity_1 = require("../heart/identity");
|
|
|
47
47
|
const os = __importStar(require("os"));
|
|
48
48
|
const channel_1 = require("./friends/channel");
|
|
49
49
|
const runtime_1 = require("../nerves/runtime");
|
|
50
|
+
const bundle_manifest_1 = require("./bundle-manifest");
|
|
50
51
|
const first_impressions_1 = require("./first-impressions");
|
|
51
52
|
const tasks_1 = require("../repertoire/tasks");
|
|
52
53
|
// Lazy-loaded psyche text cache
|
|
53
54
|
let _psycheCache = null;
|
|
55
|
+
let _senseStatusLinesCache = null;
|
|
54
56
|
function loadPsycheFile(name) {
|
|
55
57
|
try {
|
|
56
58
|
const psycheDir = path.join((0, identity_1.getAgentRoot)(), "psyche");
|
|
@@ -74,6 +76,7 @@ function loadPsyche() {
|
|
|
74
76
|
}
|
|
75
77
|
function resetPsycheCache() {
|
|
76
78
|
_psycheCache = null;
|
|
79
|
+
_senseStatusLinesCache = null;
|
|
77
80
|
}
|
|
78
81
|
const DEFAULT_ACTIVE_THRESHOLD_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
79
82
|
function resolveFriendName(friendId, friendsDir, agentName) {
|
|
@@ -194,15 +197,84 @@ function runtimeInfoSection(channel) {
|
|
|
194
197
|
lines.push(`agent: ${agentName}`);
|
|
195
198
|
lines.push(`cwd: ${process.cwd()}`);
|
|
196
199
|
lines.push(`channel: ${channel}`);
|
|
200
|
+
lines.push(`current sense: ${channel}`);
|
|
197
201
|
lines.push(`i can read and modify my own source code.`);
|
|
198
202
|
if (channel === "cli") {
|
|
199
203
|
lines.push("i introduce myself on boot with a fun random greeting.");
|
|
200
204
|
}
|
|
205
|
+
else if (channel === "bluebubbles") {
|
|
206
|
+
lines.push("i am responding in iMessage through BlueBubbles. i keep replies short and phone-native. i do not use markdown. i do not introduce myself on boot.");
|
|
207
|
+
}
|
|
201
208
|
else {
|
|
202
209
|
lines.push("i am responding in Microsoft Teams. i keep responses concise. i use markdown formatting. i do not introduce myself on boot.");
|
|
203
210
|
}
|
|
211
|
+
lines.push("");
|
|
212
|
+
lines.push(...senseRuntimeGuidance(channel));
|
|
204
213
|
return lines.join("\n");
|
|
205
214
|
}
|
|
215
|
+
function hasTextField(record, key) {
|
|
216
|
+
return typeof record?.[key] === "string" && record[key].trim().length > 0;
|
|
217
|
+
}
|
|
218
|
+
function localSenseStatusLines() {
|
|
219
|
+
if (_senseStatusLinesCache) {
|
|
220
|
+
return [..._senseStatusLinesCache];
|
|
221
|
+
}
|
|
222
|
+
const config = (0, identity_1.loadAgentConfig)();
|
|
223
|
+
const senses = config.senses ?? {
|
|
224
|
+
cli: { enabled: true },
|
|
225
|
+
teams: { enabled: false },
|
|
226
|
+
bluebubbles: { enabled: false },
|
|
227
|
+
};
|
|
228
|
+
let payload = {};
|
|
229
|
+
try {
|
|
230
|
+
const raw = fs.readFileSync((0, identity_1.getAgentSecretsPath)(), "utf-8");
|
|
231
|
+
const parsed = JSON.parse(raw);
|
|
232
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
233
|
+
payload = parsed;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
payload = {};
|
|
238
|
+
}
|
|
239
|
+
const teams = payload.teams;
|
|
240
|
+
const bluebubbles = payload.bluebubbles;
|
|
241
|
+
const configured = {
|
|
242
|
+
cli: true,
|
|
243
|
+
teams: hasTextField(teams, "clientId") && hasTextField(teams, "clientSecret") && hasTextField(teams, "tenantId"),
|
|
244
|
+
bluebubbles: hasTextField(bluebubbles, "serverUrl") && hasTextField(bluebubbles, "password"),
|
|
245
|
+
};
|
|
246
|
+
const rows = [
|
|
247
|
+
{ label: "CLI", status: "interactive" },
|
|
248
|
+
{
|
|
249
|
+
label: "Teams",
|
|
250
|
+
status: !senses.teams.enabled ? "disabled" : configured.teams ? "ready" : "needs_config",
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
label: "BlueBubbles",
|
|
254
|
+
status: !senses.bluebubbles.enabled ? "disabled" : configured.bluebubbles ? "ready" : "needs_config",
|
|
255
|
+
},
|
|
256
|
+
];
|
|
257
|
+
_senseStatusLinesCache = rows.map((row) => `- ${row.label}: ${row.status}`);
|
|
258
|
+
return [..._senseStatusLinesCache];
|
|
259
|
+
}
|
|
260
|
+
function senseRuntimeGuidance(channel) {
|
|
261
|
+
const lines = ["available senses:"];
|
|
262
|
+
lines.push(...localSenseStatusLines());
|
|
263
|
+
lines.push("sense states:");
|
|
264
|
+
lines.push("- interactive = available when opened by the user instead of kept running by the daemon");
|
|
265
|
+
lines.push("- disabled = turned off in agent.json");
|
|
266
|
+
lines.push("- needs_config = enabled but missing required secrets.json values");
|
|
267
|
+
lines.push("- ready = enabled and configured; `ouro up` should bring it online");
|
|
268
|
+
lines.push("- running = enabled and currently active");
|
|
269
|
+
lines.push("- error = enabled but unhealthy");
|
|
270
|
+
lines.push("If asked how to enable another sense, I explain the relevant agent.json senses entry and required secrets.json fields instead of guessing.");
|
|
271
|
+
lines.push("teams setup truth: enable `senses.teams.enabled`, then provide `teams.clientId`, `teams.clientSecret`, and `teams.tenantId` in secrets.json.");
|
|
272
|
+
lines.push("bluebubbles setup truth: enable `senses.bluebubbles.enabled`, then provide `bluebubbles.serverUrl` and `bluebubbles.password` in secrets.json.");
|
|
273
|
+
if (channel === "cli") {
|
|
274
|
+
lines.push("cli is interactive: it is available when the user opens it, not something `ouro up` daemonizes.");
|
|
275
|
+
}
|
|
276
|
+
return lines;
|
|
277
|
+
}
|
|
206
278
|
function providerSection() {
|
|
207
279
|
return `## my provider\n${(0, core_1.getProviderDisplayLabel)()}`;
|
|
208
280
|
}
|
|
@@ -210,8 +282,8 @@ function dateSection() {
|
|
|
210
282
|
const today = new Date().toISOString().slice(0, 10);
|
|
211
283
|
return `current date: ${today}`;
|
|
212
284
|
}
|
|
213
|
-
function toolsSection(channel, options) {
|
|
214
|
-
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);
|
|
215
287
|
const activeTools = (options?.toolChoiceRequired ?? true) ? [...channelTools, tools_1.finalAnswerTool] : channelTools;
|
|
216
288
|
const list = activeTools
|
|
217
289
|
.map((t) => `- ${t.function.name}: ${t.function.description}`)
|
|
@@ -316,6 +388,8 @@ async function buildSystem(channel = "cli", options, context) {
|
|
|
316
388
|
message: "buildSystem started",
|
|
317
389
|
meta: { channel, has_context: Boolean(context), tool_choice_required: Boolean(options?.toolChoiceRequired) },
|
|
318
390
|
});
|
|
391
|
+
// Backfill bundle-meta.json for existing agents that don't have one
|
|
392
|
+
(0, bundle_manifest_1.backfillBundleMeta)((0, identity_1.getAgentRoot)());
|
|
319
393
|
const system = [
|
|
320
394
|
soulSection(),
|
|
321
395
|
identitySection(),
|
|
@@ -325,7 +399,7 @@ async function buildSystem(channel = "cli", options, context) {
|
|
|
325
399
|
runtimeInfoSection(channel),
|
|
326
400
|
providerSection(),
|
|
327
401
|
dateSection(),
|
|
328
|
-
toolsSection(channel, options),
|
|
402
|
+
toolsSection(channel, options, context),
|
|
329
403
|
skillsSection(),
|
|
330
404
|
taskBoardSection(),
|
|
331
405
|
buildSessionSummary({
|
|
@@ -5,20 +5,33 @@ const config_1 = require("../heart/config");
|
|
|
5
5
|
const nerves_1 = require("../nerves");
|
|
6
6
|
const runtime_1 = require("./runtime");
|
|
7
7
|
const runtime_2 = require("./runtime");
|
|
8
|
+
const LEVEL_PRIORITY = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
9
|
+
/** Wrap a sink so it only receives events at or above the given level. */
|
|
10
|
+
/* v8 ignore start -- internal filter plumbing, exercised via integration @preserve */
|
|
11
|
+
function filterSink(sink, minLevel) {
|
|
12
|
+
const minPriority = LEVEL_PRIORITY[minLevel] ?? 0;
|
|
13
|
+
return (entry) => {
|
|
14
|
+
if ((LEVEL_PRIORITY[entry.level] ?? 0) >= minPriority)
|
|
15
|
+
sink(entry);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
8
18
|
function resolveCliSinks(sinks) {
|
|
9
19
|
const requested = sinks && sinks.length > 0 ? sinks : ["terminal", "ndjson"];
|
|
10
20
|
return [...new Set(requested)];
|
|
11
21
|
}
|
|
12
22
|
function configureCliRuntimeLogger(_friendId, options = {}) {
|
|
13
23
|
const sinkKinds = resolveCliSinks(options.sinks);
|
|
24
|
+
const level = options.level ?? "info";
|
|
14
25
|
const sinks = sinkKinds.map((sinkKind) => {
|
|
15
26
|
if (sinkKind === "terminal") {
|
|
16
|
-
|
|
27
|
+
// Terminal only shows warnings and errors — INFO is too noisy
|
|
28
|
+
// for an interactive session. Full detail goes to the ndjson file.
|
|
29
|
+
return filterSink((0, nerves_1.createTerminalSink)(), "warn");
|
|
17
30
|
}
|
|
18
31
|
return (0, nerves_1.createNdjsonFileSink)((0, config_1.logPath)("cli", "runtime"));
|
|
19
32
|
});
|
|
20
33
|
const logger = (0, nerves_1.createLogger)({
|
|
21
|
-
level
|
|
34
|
+
level,
|
|
22
35
|
sinks,
|
|
23
36
|
});
|
|
24
37
|
(0, runtime_2.setRuntimeLogger)(logger);
|
|
@@ -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; } });
|