@ouro.bot/cli 0.1.0-alpha.927 → 0.1.0-alpha.929
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/changelog.json +15 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/attachments/image-normalize.js +8 -1
- package/dist/repertoire/guardrails.js +3 -0
- package/dist/repertoire/tools-teams.js +38 -0
- package/dist/repertoire/tools.js +5 -2
- package/dist/senses/shepherd/judge.js +8 -1
- package/dist/senses/shepherd/sense.js +113 -7
- package/dist/senses/teams-image.js +119 -0
- package/dist/senses/teams.js +3 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.929",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Shepherd resumes network-stopped coding sessions once after independently confirmed offline-to-online recovery, with a fresh judgment and existing focus, pause, changed-screen and loop guards.",
|
|
8
|
+
"Connectivity recovery never presses approval keys or blindly retries uncertain writes; changed sessions, background work and uncertain terminal sends are not replayed."
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"version": "0.1.0-alpha.928",
|
|
13
|
+
"changes": [
|
|
14
|
+
"Teams can send private local PNG and JPEG images into the current conversation, with bounded inline bytes, family trust, no-send enforcement and honest activity receipts.",
|
|
15
|
+
"Preserves the existing Teams integration schema order when adding inline image delivery.",
|
|
16
|
+
"Records private-image preparation and Teams acceptance in structured telemetry without logging image bytes, paths or captions."
|
|
17
|
+
]
|
|
18
|
+
},
|
|
4
19
|
{
|
|
5
20
|
"version": "0.1.0-alpha.927",
|
|
6
21
|
"changes": [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<?xml version="1.0"?>
|
|
2
2
|
<Container version="2">
|
|
3
3
|
<Name>ouro-butler</Name>
|
|
4
|
-
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.
|
|
4
|
+
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.929</Repository>
|
|
5
5
|
<Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
|
|
6
6
|
<Network>host</Network>
|
|
7
7
|
<Shell>sh</Shell>
|
|
@@ -101,10 +101,14 @@ async function defaultEncodeVariant(input) {
|
|
|
101
101
|
}
|
|
102
102
|
async function normalizeImageForVision(params) {
|
|
103
103
|
const maxVlmBytes = params.maxVlmBytes ?? exports.MAX_VLM_IMAGE_BYTES;
|
|
104
|
+
if (params.maxEdge !== undefined && (!Number.isInteger(params.maxEdge) || params.maxEdge < 1)) {
|
|
105
|
+
throw new Error("Image maximum edge must be a positive integer");
|
|
106
|
+
}
|
|
104
107
|
const originalMimeType = params.attachment.mimeType?.trim().toLowerCase();
|
|
105
108
|
const originalByteCount = params.attachment.byteCount;
|
|
106
109
|
const outputDir = materializedOutputDir(params.agentRoot, params.attachment.id);
|
|
107
110
|
if (originalMimeType
|
|
111
|
+
&& params.maxEdge === undefined
|
|
108
112
|
&& SAFE_VLM_MIME_TYPES.has(originalMimeType)
|
|
109
113
|
&& typeof originalByteCount === "number"
|
|
110
114
|
&& originalByteCount <= maxVlmBytes) {
|
|
@@ -149,8 +153,11 @@ async function normalizeImageForVision(params) {
|
|
|
149
153
|
probe = await probeImage({ sourcePath: workingSourcePath });
|
|
150
154
|
}
|
|
151
155
|
const targetFormats = probe.hasAlpha ? ["png", "jpeg"] : ["jpeg"];
|
|
156
|
+
const requestedMaxEdge = params.maxEdge;
|
|
157
|
+
const sizes = requestedMaxEdge === undefined ? SIZE_STEPS
|
|
158
|
+
: [...new Set([requestedMaxEdge, ...SIZE_STEPS.filter((size) => size < requestedMaxEdge)])];
|
|
152
159
|
for (const targetFormat of targetFormats) {
|
|
153
|
-
for (const maxEdge of
|
|
160
|
+
for (const maxEdge of sizes) {
|
|
154
161
|
const qualities = targetFormat === "jpeg" ? JPEG_QUALITIES : [undefined];
|
|
155
162
|
for (const quality of qualities) {
|
|
156
163
|
const variant = await encodeVariant({
|
|
@@ -1184,6 +1184,9 @@ function checkFirstClassMcpTrust(context) {
|
|
|
1184
1184
|
return allow;
|
|
1185
1185
|
}
|
|
1186
1186
|
function checkTrustLevelGuardrails(toolName, args, context) {
|
|
1187
|
+
if (toolName === "teams_send_image" && context.trustLevel !== "family") {
|
|
1188
|
+
return deny("sending local images in Teams requires family trust.");
|
|
1189
|
+
}
|
|
1187
1190
|
const cacheSyncResult = mailCacheSyncShellGuardrail(toolName, args, context);
|
|
1188
1191
|
if (!cacheSyncResult.allowed)
|
|
1189
1192
|
return cacheSyncResult;
|
|
@@ -9,6 +9,7 @@ const ado_client_1 = require("./ado-client");
|
|
|
9
9
|
const graph_endpoints_json_1 = __importDefault(require("./data/graph-endpoints.json"));
|
|
10
10
|
const ado_endpoints_json_1 = __importDefault(require("./data/ado-endpoints.json"));
|
|
11
11
|
const runtime_1 = require("../nerves/runtime");
|
|
12
|
+
const teams_image_1 = require("../senses/teams-image");
|
|
12
13
|
const MUTATE_METHODS = ["POST", "PATCH", "DELETE"];
|
|
13
14
|
const DEFAULT_ADO_QUERY = "SELECT [System.Id], [System.Title], [System.State], [System.AssignedTo] FROM WorkItems WHERE [System.AssignedTo] = @Me AND [System.State] <> 'Closed' ORDER BY [System.ChangedDate] DESC";
|
|
14
15
|
exports.teamsToolDefinitions = [
|
|
@@ -283,6 +284,43 @@ exports.teamsToolDefinitions = [
|
|
|
283
284
|
integration: "ado",
|
|
284
285
|
summaryKeys: ["query"],
|
|
285
286
|
},
|
|
287
|
+
{
|
|
288
|
+
tool: {
|
|
289
|
+
type: "function",
|
|
290
|
+
function: {
|
|
291
|
+
name: "teams_send_image",
|
|
292
|
+
description: "Send a local PNG or JPEG image into the current Teams conversation, without publishing it or using OneDrive. Requires family trust. Images are resized/compressed for inline delivery. An activity receipt confirms Teams acceptance, not client rendering.",
|
|
293
|
+
parameters: {
|
|
294
|
+
type: "object",
|
|
295
|
+
properties: {
|
|
296
|
+
path: { type: "string", description: "Absolute path to a local PNG or JPEG image" },
|
|
297
|
+
caption: { type: "string", maxLength: teams_image_1.MAX_TEAMS_IMAGE_CAPTION_LENGTH, description: "Optional short caption (up to 1024 characters)" },
|
|
298
|
+
},
|
|
299
|
+
required: ["path"],
|
|
300
|
+
additionalProperties: false,
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
handler: async (args, ctx) => {
|
|
305
|
+
if (ctx?.noSend || ctx?.habitSession || ctx?.autonomousTurnKind) {
|
|
306
|
+
throw new Error("Teams image sending is unavailable in no-send or autonomous turns");
|
|
307
|
+
}
|
|
308
|
+
if (ctx?.context?.friend.trustLevel !== "family" || ctx.context.channel.channel !== "teams") {
|
|
309
|
+
throw new Error("Sending local images in Teams requires a family-trusted Teams conversation");
|
|
310
|
+
}
|
|
311
|
+
if (!ctx.teamsSendImage || !ctx.agentName || !ctx.agentRoot) {
|
|
312
|
+
throw new Error("Teams image sending requires an authenticated current-conversation sender and agent owner");
|
|
313
|
+
}
|
|
314
|
+
const caption = args.caption ?? "";
|
|
315
|
+
if (caption.length > teams_image_1.MAX_TEAMS_IMAGE_CAPTION_LENGTH)
|
|
316
|
+
throw new Error("Teams image caption exceeds 1024 characters");
|
|
317
|
+
const attachment = await (0, teams_image_1.prepareTeamsImage)({ path: args.path, agentName: ctx.agentName, agentRoot: ctx.agentRoot });
|
|
318
|
+
const activityId = await ctx.teamsSendImage(attachment, caption);
|
|
319
|
+
return `image accepted by Teams in the current conversation (activity ${activityId}); client rendering is not verified`;
|
|
320
|
+
},
|
|
321
|
+
summaryKeys: ["path"],
|
|
322
|
+
riskProfile: { mutates: "external_side_effect", risk: "high", reason: "sends local image bytes to the current Teams conversation" },
|
|
323
|
+
},
|
|
286
324
|
];
|
|
287
325
|
function searchEndpoints(entries, query) {
|
|
288
326
|
(0, runtime_1.emitNervesEvent)({
|
package/dist/repertoire/tools.js
CHANGED
|
@@ -164,7 +164,8 @@ function selectToolsForChannel(capabilities, toolPreferences, _context, provider
|
|
|
164
164
|
}
|
|
165
165
|
else {
|
|
166
166
|
const available = new Set(capabilities?.availableIntegrations ?? []);
|
|
167
|
-
const integrations =
|
|
167
|
+
const integrations = [...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions]
|
|
168
|
+
.filter((definition) => available.size > 0 || definition.tool.function.name === "teams_send_image")
|
|
168
169
|
.filter((definition) => definition.integration ? available.has(definition.integration) : capabilities?.channel === "teams")
|
|
169
170
|
.map((definition) => {
|
|
170
171
|
const preference = definition.integration ? toolPreferences?.[definition.integration] : undefined;
|
|
@@ -197,7 +198,9 @@ function selectToolsForChannel(capabilities, toolPreferences, _context, provider
|
|
|
197
198
|
ordinary.push(tools_surface_1.surfaceToolDefinition);
|
|
198
199
|
}
|
|
199
200
|
}
|
|
200
|
-
ordinary = ordinary.filter((definition) => (
|
|
201
|
+
ordinary = ordinary.filter((definition) => (definition.tool.function.name !== "teams_send_image" || (!context?.noSend && !context?.habitSession && !context?.autonomousTurnKind))
|
|
202
|
+
&&
|
|
203
|
+
(!relationship || relationship.advertisedToolNames.includes(definition.tool.function.name))
|
|
201
204
|
&& (!definition.requiredCapability || providerCapabilities?.has(definition.requiredCapability) === true));
|
|
202
205
|
const engine = [
|
|
203
206
|
...(context?.noSend ? [] : [tools_flow_1.ponderTool]),
|
|
@@ -69,6 +69,8 @@ function parseVerdict(content) {
|
|
|
69
69
|
const reason = typeof value.reason === "string" && value.reason.trim() ? value.reason.trim().replace(/\s+/g, " ").slice(0, 300) : "no reason given";
|
|
70
70
|
if (kind === "working" && value.news === true)
|
|
71
71
|
return { kind, reply: null, reason, news: true };
|
|
72
|
+
if (kind === "waiting" && value.waitingOnNetwork === true)
|
|
73
|
+
return { kind, reply: null, reason, waitingOnNetwork: true };
|
|
72
74
|
if (kind !== "premature")
|
|
73
75
|
return { kind, reply: null, reason };
|
|
74
76
|
// Some models write the reply as a bare string instead of {"text": ...} or {"key": ...}; a string that is exactly a key is one.
|
|
@@ -93,6 +95,7 @@ function judgeMessages(packet) {
|
|
|
93
95
|
`- gate: the next step truly needs ${packet.human}: something sent or said in their voice; a decision that is theirs; their money, credentials or accounts only they can act in; an irreversible act; or real ambiguity about what they want or what the agent is allowed to do.`,
|
|
94
96
|
"- done: the work is finished and delivered, with evidence, and nothing is left the agent could do itself.",
|
|
95
97
|
"- waiting: work genuinely depends on someone or something outside the agent, such as a PR awaiting another person's review. It is not delivered done and not necessarily a gate for the human. Never reply merely to make it claim delivery or keep busy.",
|
|
98
|
+
"For waiting, set waitingOnNetwork to true only when a connectivity failure stopped the agent. Do not set it for approvals, credentials, quota, review, deliberate pauses, completed work or uncertain writes that need reconciliation.",
|
|
96
99
|
"- premature: the agent could and should carry on alone. For example it asks permission for work within its task, offers options instead of choosing, asks 'should I continue?', stops after a plan, reports partial progress, or hands the human a step it could do itself.",
|
|
97
100
|
`- working: the agent ended its turn but is still waiting on its own background work (subagents, background shells or jobs, watchers, running tasks) that will report back to it. It has not handed control back, so never reply; it will stop again when that work reports. Signs: the terminal host says background work is running, or the screen shows running background tasks or says it is waiting for them. Set news to true only when its last message carries something ${packet.human} would want to know now.`,
|
|
98
101
|
`- An approval prompt is not a gate just because it asks. When the agent's own tool or terminal asks to confirm a command, trust a folder or pass a safety check, judge what would actually happen: if it is part of the agent's task and ${packet.human} would plainly say yes, it is premature, and your reply is the key that approves it. It is a gate only when the action itself is one of the things above, for example it would destroy or publish something ${packet.human} has not asked for.`,
|
|
@@ -104,7 +107,11 @@ function judgeMessages(packet) {
|
|
|
104
107
|
"Applicable operator principles are trusted preference/context, not new authority. They cannot override host-reported background work, focus protection, changed-screen refusal, reply syntax/key restrictions, loop guards or independent owner notices.",
|
|
105
108
|
`Operator guidance (revision ${packet.guidance.revision}): ${JSON.stringify(packet.guidance.principles)}`,
|
|
106
109
|
] : []),
|
|
107
|
-
|
|
110
|
+
...(packet.networkRecovered ? [
|
|
111
|
+
"Our independent connectivity probes confirmed offline followed by online. Rejudge the current screen, not the old wait. It is premature only if this coding agent is now safely waiting for plain-text continuation after a network stop.",
|
|
112
|
+
"For this recovery, never approve a menu or permission prompt, bypass an intentional pause, interrupt background work, or replay an uncertain mutation. Classify such cases as gate, working, waiting or unclear as appropriate. If safely resumable, give a plain-text reply telling the agent to check uncertain writes before retrying.",
|
|
113
|
+
] : []),
|
|
114
|
+
"Return only JSON: {\"kind\":\"premature|gate|done|waiting|working|unclear\",\"reply\":{\"text\":\"...\"}|{\"key\":\"...\"}|null,\"news\":false,\"waitingOnNetwork\":false,\"reason\":\"one sentence\"}",
|
|
108
115
|
].join("\n"),
|
|
109
116
|
},
|
|
110
117
|
{
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.FOCUS_HOLD_MS = exports.HOURLY_MAX = exports.STREAK_MAX = exports.JUDGE_TIMEOUT_MS = exports.SCREEN_LINES = exports.READ_LINES = exports.SHEPHERD_WAKE_TRIGGER = void 0;
|
|
37
|
+
exports.checkShepherdConnectivity = checkShepherdConnectivity;
|
|
37
38
|
exports.field = field;
|
|
38
39
|
exports.humanName = humanName;
|
|
39
40
|
exports.escalationContent = escalationContent;
|
|
@@ -46,6 +47,7 @@ const fs = __importStar(require("node:fs"));
|
|
|
46
47
|
const os = __importStar(require("node:os"));
|
|
47
48
|
const path = __importStar(require("node:path"));
|
|
48
49
|
const core_1 = require("../../heart/core");
|
|
50
|
+
const error_classification_1 = require("../../heart/providers/error-classification");
|
|
49
51
|
const socket_client_1 = require("../../heart/daemon/socket-client");
|
|
50
52
|
const identity_1 = require("../../heart/identity");
|
|
51
53
|
const runtime_credentials_1 = require("../../heart/runtime-credentials");
|
|
@@ -79,6 +81,24 @@ exports.FOCUS_HOLD_MS = 60_000;
|
|
|
79
81
|
const ESCALATION_TTL_MS = 30 * 60_000;
|
|
80
82
|
const WAKE_DELAY_MS = 2_000;
|
|
81
83
|
const FIELD_MAX = 200;
|
|
84
|
+
const CONNECTIVITY_POLL_MS = 15_000;
|
|
85
|
+
const NETWORK_RESUME = "Connectivity has recovered. Continue the existing task only if safe. Check uncertain writes before retrying. Do not bypass approvals or deliberate pauses.";
|
|
86
|
+
/** No credentials, terminal-selected URLs or redirects; mixed evidence cannot establish a transition. */
|
|
87
|
+
async function checkShepherdConnectivity() {
|
|
88
|
+
const results = await Promise.all(["https://api.github.com/", "https://registry.npmjs.org/"].map(async (url) => {
|
|
89
|
+
try {
|
|
90
|
+
const response = await fetch(url, { method: "HEAD", redirect: "error", credentials: "omit", signal: AbortSignal.timeout(5_000) });
|
|
91
|
+
return response.ok ? "online" : "unknown";
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (error instanceof Error && ((0, error_classification_1.isNetworkError)(error) || error.name === "TimeoutError"))
|
|
95
|
+
return "offline";
|
|
96
|
+
(0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.shepherd_connectivity_error", message: "connectivity probe failed without network evidence", meta: {} });
|
|
97
|
+
return "unknown";
|
|
98
|
+
}
|
|
99
|
+
}));
|
|
100
|
+
return results.every(result => result === "online") ? "online" : results.every(result => result === "offline") ? "offline" : "unknown";
|
|
101
|
+
}
|
|
82
102
|
/** Untrusted terminal text, kept to one bounded line. */
|
|
83
103
|
function field(value, max = FIELD_MAX) {
|
|
84
104
|
const flat = value.replace(/[\p{Cc}\p{Zl}\p{Zp}\s]+/gu, " ").trim();
|
|
@@ -187,6 +207,64 @@ async function startShepherdSenseApp(options) {
|
|
|
187
207
|
const wakeIds = [];
|
|
188
208
|
let cancelWake = null;
|
|
189
209
|
let chain = Promise.resolve();
|
|
210
|
+
const networkWaiting = new Map();
|
|
211
|
+
const latestReturns = new Map();
|
|
212
|
+
const checkConnectivity = options.checkConnectivity ?? checkShepherdConnectivity;
|
|
213
|
+
let cancelConnectivity = null;
|
|
214
|
+
let recoveryGeneration = 0;
|
|
215
|
+
let stopped = false;
|
|
216
|
+
function scheduleConnectivity() {
|
|
217
|
+
if (stopped || networkWaiting.size === 0 || cancelConnectivity)
|
|
218
|
+
return;
|
|
219
|
+
cancelConnectivity = schedule(() => {
|
|
220
|
+
cancelConnectivity = null;
|
|
221
|
+
chain = chain.then(checkRecovery);
|
|
222
|
+
}, CONNECTIVITY_POLL_MS);
|
|
223
|
+
}
|
|
224
|
+
async function checkRecovery() {
|
|
225
|
+
if (stopped || networkWaiting.size === 0)
|
|
226
|
+
return;
|
|
227
|
+
try {
|
|
228
|
+
const connectivity = await checkConnectivity();
|
|
229
|
+
if (stopped)
|
|
230
|
+
return;
|
|
231
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.shepherd_connectivity_checked", message: "checked connectivity for stopped sessions", meta: { connectivity, pending: networkWaiting.size } });
|
|
232
|
+
if (connectivity === "offline") {
|
|
233
|
+
for (const candidate of networkWaiting.values())
|
|
234
|
+
candidate.offlineConfirmed = true;
|
|
235
|
+
}
|
|
236
|
+
else if (connectivity === "online") {
|
|
237
|
+
const candidates = [...networkWaiting.values()].filter(candidate => candidate.offlineConfirmed);
|
|
238
|
+
if (candidates.length === 0)
|
|
239
|
+
return;
|
|
240
|
+
recoveryGeneration += 1;
|
|
241
|
+
// Consume before any send: a failed send may already have reached the terminal.
|
|
242
|
+
for (const candidate of candidates)
|
|
243
|
+
networkWaiting.delete(candidate.event.sessionId);
|
|
244
|
+
const sessions = await host.list();
|
|
245
|
+
for (const candidate of candidates) {
|
|
246
|
+
if (stopped)
|
|
247
|
+
break;
|
|
248
|
+
if (latestReturns.get(candidate.event.sessionId) !== candidate.event ||
|
|
249
|
+
!sessions.some(session => session.id === candidate.event.sessionId))
|
|
250
|
+
continue;
|
|
251
|
+
await handle(candidate.event, { screen: candidate.screen, generation: recoveryGeneration });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
(0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.shepherd_recovery_error", message: "could not check stopped sessions for connectivity recovery", meta: { agent, error: (0, redact_1.redactSecrets)(String(error)) } });
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
scheduleConnectivity();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function rememberNetworkWait(event, screen) {
|
|
263
|
+
if (stopped || event.backgroundWork || latestReturns.get(event.sessionId) !== event)
|
|
264
|
+
return;
|
|
265
|
+
networkWaiting.set(event.sessionId, { event, screen, offlineConfirmed: false });
|
|
266
|
+
scheduleConnectivity();
|
|
267
|
+
}
|
|
190
268
|
const guardFor = (session) => {
|
|
191
269
|
let entry = guard.get(session);
|
|
192
270
|
if (!entry)
|
|
@@ -201,9 +279,9 @@ async function startShepherdSenseApp(options) {
|
|
|
201
279
|
}, WAKE_DELAY_MS);
|
|
202
280
|
};
|
|
203
281
|
/** Judges one return and acts on it. Every outcome, including a failure, is one log line. */
|
|
204
|
-
async function handle(event) {
|
|
282
|
+
async function handle(event, recovery) {
|
|
205
283
|
const record = {
|
|
206
|
-
at: new Date(now()).toISOString(), host: host.name, session: event.sessionId, transition: event.transitionId,
|
|
284
|
+
at: new Date(now()).toISOString(), host: host.name, session: event.sessionId, transition: recovery ? `network:${recovery.generation}:${event.transitionId}` : event.transitionId,
|
|
207
285
|
agent: event.agent, cwd: event.cwd, task: null, kind: "unclear", action: "let_through", reason: "", reply: null, latencyMs: null, inputTokens: null,
|
|
208
286
|
...(event.title !== undefined ? { title: event.title, workspaceTitle: event.workspaceTitle } : {}),
|
|
209
287
|
};
|
|
@@ -227,14 +305,28 @@ async function startShepherdSenseApp(options) {
|
|
|
227
305
|
const text = (0, redact_1.redactSecrets)(await host.read(event.sessionId, exports.READ_LINES));
|
|
228
306
|
record.task = [...text.matchAll(/Desk-Task:\s*(\S+)/g)].pop()?.[1] ?? null;
|
|
229
307
|
const screen = (0, redact_1.screenTail)(text, exports.SCREEN_LINES);
|
|
308
|
+
if (recovery && screen !== recovery.screen) {
|
|
309
|
+
record.reason = "the screen changed since the network stop, so recovery was not sent";
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
230
312
|
const target = { host: host.name, session: event.sessionId, cwd: event.cwd, task: record.task };
|
|
231
313
|
const guidance = guidanceFor(target);
|
|
232
314
|
record.guidanceRevision = guidance.revision;
|
|
233
|
-
|
|
315
|
+
let verdict;
|
|
316
|
+
try {
|
|
317
|
+
verdict = await judge({ human, agent: event.agent, cwd: event.cwd, task: record.task, lastBody: event.lastBody ? (0, redact_1.redactSecrets)(event.lastBody) : null, backgroundWork: event.backgroundWork, screen, guidance, ...(recovery ? { networkRecovered: true } : {}) });
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
if (!recovery && error instanceof Error && (0, error_classification_1.isNetworkError)(error))
|
|
321
|
+
rememberNetworkWait(event, screen);
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
234
324
|
// The host's own word that background work is running outranks a reading of the screen: such a stop is never a return.
|
|
235
325
|
const kind = event.backgroundWork ? "working" : verdict.kind;
|
|
236
326
|
const news = verdict.news === true || (kind === "working" && verdict.kind === "gate");
|
|
237
327
|
Object.assign(record, { kind, reason: verdict.reason, latencyMs: verdict.latencyMs, inputTokens: verdict.inputTokens });
|
|
328
|
+
if (!recovery && kind === "waiting" && verdict.waitingOnNetwork)
|
|
329
|
+
rememberNetworkWait(event, screen);
|
|
238
330
|
// A terminal that keeps going quiet without needing anything is checked less often.
|
|
239
331
|
host.pace?.(event.sessionId, kind === "working" || kind === "unclear");
|
|
240
332
|
state.answers = state.answers.filter((at) => now() - at < 3_600_000);
|
|
@@ -247,7 +339,10 @@ async function startShepherdSenseApp(options) {
|
|
|
247
339
|
await host.signal(event.sessionId, field(`${event.agent ?? "agent"} waiting on its background work: ${verdict.reason}`, 120));
|
|
248
340
|
}
|
|
249
341
|
if (kind === "premature" && verdict.reply) {
|
|
250
|
-
if (
|
|
342
|
+
if (recovery && "key" in verdict.reply) {
|
|
343
|
+
record.reason = `connectivity recovery never presses approval or menu keys: ${verdict.reason}`;
|
|
344
|
+
}
|
|
345
|
+
else if (guidance.paused) {
|
|
251
346
|
record.reason = `Shepherd guidance has this session paused, so nothing was sent: ${verdict.reason}`;
|
|
252
347
|
}
|
|
253
348
|
else if (state.streak >= exports.STREAK_MAX || state.answers.length >= exports.HOURLY_MAX) {
|
|
@@ -264,15 +359,18 @@ async function startShepherdSenseApp(options) {
|
|
|
264
359
|
else {
|
|
265
360
|
// No awaited host reads between this final check and initiating the send.
|
|
266
361
|
const latest = guidanceFor(target);
|
|
267
|
-
if (
|
|
362
|
+
if (stopped || (recovery && (latestReturns.get(event.sessionId) !== event || now() - state.focusedAt < exports.FOCUS_HOLD_MS))) {
|
|
363
|
+
record.reason = `Shepherd stopped, the human focused this session, or it returned again while judging, so nothing was sent: ${verdict.reason}`;
|
|
364
|
+
}
|
|
365
|
+
else if (latest.paused || latest.revision !== guidance.revision) {
|
|
268
366
|
record.reason = `Shepherd guidance paused or changed while judging (revision ${guidance.revision} to ${latest.revision}), so nothing was sent: ${verdict.reason}`;
|
|
269
367
|
}
|
|
270
368
|
else {
|
|
271
369
|
if ("key" in verdict.reply)
|
|
272
370
|
await host.key(event.sessionId, verdict.reply.key);
|
|
273
371
|
else
|
|
274
|
-
await host.prompt(event.sessionId, `[Ouro for ${human}] ${verdict.reply.text}`);
|
|
275
|
-
Object.assign(record, { action: "respond", reply: "key" in verdict.reply ? `key:${verdict.reply.key}` : verdict.reply.text });
|
|
372
|
+
await host.prompt(event.sessionId, `[Ouro for ${human}] ${recovery ? NETWORK_RESUME : verdict.reply.text}`);
|
|
373
|
+
Object.assign(record, { action: "respond", reply: "key" in verdict.reply ? `key:${verdict.reply.key}` : recovery ? NETWORK_RESUME : verdict.reply.text });
|
|
276
374
|
state.streak += 1;
|
|
277
375
|
state.answers.push(now());
|
|
278
376
|
await host.signal(event.sessionId, field(`answered for ${human}: ${verdict.reason}`, 120));
|
|
@@ -321,9 +419,14 @@ async function startShepherdSenseApp(options) {
|
|
|
321
419
|
}
|
|
322
420
|
const watcher = host.watch({
|
|
323
421
|
returned: (event) => {
|
|
422
|
+
if (stopped)
|
|
423
|
+
return;
|
|
424
|
+
networkWaiting.delete(event.sessionId);
|
|
425
|
+
latestReturns.set(event.sessionId, event);
|
|
324
426
|
chain = chain.then(() => handle(event));
|
|
325
427
|
},
|
|
326
428
|
focused: (session) => {
|
|
429
|
+
networkWaiting.delete(session);
|
|
327
430
|
const state = guardFor(session);
|
|
328
431
|
Object.assign(state, { streak: 0, tripped: false, focusedAt: now() });
|
|
329
432
|
},
|
|
@@ -331,8 +434,11 @@ async function startShepherdSenseApp(options) {
|
|
|
331
434
|
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.shepherd_started", message: "Shepherd started", meta: { agent, host: host.name } });
|
|
332
435
|
return {
|
|
333
436
|
stop() {
|
|
437
|
+
stopped = true;
|
|
438
|
+
networkWaiting.clear();
|
|
334
439
|
watcher.close();
|
|
335
440
|
cancelWake?.();
|
|
441
|
+
cancelConnectivity?.();
|
|
336
442
|
notifier?.stop();
|
|
337
443
|
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.shepherd_stopped", message: "Shepherd stopped", meta: { agent } });
|
|
338
444
|
},
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.MAX_TEAMS_IMAGE_CAPTION_LENGTH = exports.MAX_TEAMS_INLINE_IMAGE_BYTES = void 0;
|
|
37
|
+
exports.prepareTeamsImage = prepareTeamsImage;
|
|
38
|
+
exports.createTeamsImageSender = createTeamsImageSender;
|
|
39
|
+
const fs = __importStar(require("node:fs/promises"));
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const image_normalize_1 = require("../heart/attachments/image-normalize");
|
|
42
|
+
const cli_local_file_1 = require("../heart/attachments/sources/cli-local-file");
|
|
43
|
+
const runtime_1 = require("../nerves/runtime");
|
|
44
|
+
exports.MAX_TEAMS_INLINE_IMAGE_BYTES = 56 * 1024;
|
|
45
|
+
exports.MAX_TEAMS_IMAGE_CAPTION_LENGTH = 1024;
|
|
46
|
+
function imageMime(bytes) {
|
|
47
|
+
if (bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])))
|
|
48
|
+
return "image/png";
|
|
49
|
+
if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255)
|
|
50
|
+
return "image/jpeg";
|
|
51
|
+
throw new Error("Teams image must contain PNG or JPEG bytes");
|
|
52
|
+
}
|
|
53
|
+
async function readBoundedImage(filePath, maxBytes) {
|
|
54
|
+
const file = await fs.open(filePath, "r");
|
|
55
|
+
try {
|
|
56
|
+
const stat = await file.stat();
|
|
57
|
+
if (!stat.isFile() || stat.size === 0 || stat.size > maxBytes) {
|
|
58
|
+
throw new Error(`Teams image must be a nonempty regular file under ${maxBytes} bytes`);
|
|
59
|
+
}
|
|
60
|
+
// Bound the actual read as well as the stat, including a file that grows after the stat.
|
|
61
|
+
const bytes = Buffer.alloc(stat.size + 1);
|
|
62
|
+
const { bytesRead } = await file.read(bytes, 0, bytes.length, 0);
|
|
63
|
+
if (bytesRead !== stat.size)
|
|
64
|
+
throw new Error("Teams image changed while reading; nothing was sent");
|
|
65
|
+
return bytes.subarray(0, bytesRead);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
await file.close();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function prepareTeamsImage(input) {
|
|
72
|
+
if (!path.isAbsolute(input.path))
|
|
73
|
+
throw new Error("Teams image path must be absolute");
|
|
74
|
+
const original = await readBoundedImage(input.path, image_normalize_1.MAX_ATTACHMENT_DOWNLOAD_BYTES_IMAGE);
|
|
75
|
+
const mimeType = imageMime(original);
|
|
76
|
+
const attachment = (0, cli_local_file_1.buildCliLocalFileAttachmentRecord)({ path: input.path, mimeType, byteCount: original.length });
|
|
77
|
+
const normalized = await (0, image_normalize_1.normalizeImageForVision)({
|
|
78
|
+
attachment, sourcePath: input.path, agentName: input.agentName, agentRoot: input.agentRoot,
|
|
79
|
+
maxEdge: 1024, maxVlmBytes: exports.MAX_TEAMS_INLINE_IMAGE_BYTES,
|
|
80
|
+
});
|
|
81
|
+
const bytes = await readBoundedImage(normalized.path, exports.MAX_TEAMS_INLINE_IMAGE_BYTES);
|
|
82
|
+
const contentType = imageMime(bytes);
|
|
83
|
+
if (normalized.mimeType !== contentType)
|
|
84
|
+
throw new Error("Normalized Teams image MIME does not match its bytes");
|
|
85
|
+
(0, runtime_1.emitNervesEvent)({
|
|
86
|
+
component: "channels", event: "channel.teams_image_prepared",
|
|
87
|
+
message: "private image prepared for inline Teams delivery",
|
|
88
|
+
meta: { mimeType: contentType, byteCount: bytes.length, maxEdge: 1024 },
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
contentType,
|
|
92
|
+
contentUrl: `data:${contentType};base64,${bytes.toString("base64")}`,
|
|
93
|
+
name: `${path.basename(input.path, path.extname(input.path)).slice(0, 100)}${contentType === "image/png" ? ".png" : ".jpg"}`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function createTeamsImageSender(send, replyToId, entities = []) {
|
|
97
|
+
return async (attachment, caption) => {
|
|
98
|
+
if (caption.length > exports.MAX_TEAMS_IMAGE_CAPTION_LENGTH)
|
|
99
|
+
throw new Error("Teams image caption exceeds 1024 characters");
|
|
100
|
+
const message = {
|
|
101
|
+
type: "message", text: caption, replyToId, attachments: [attachment],
|
|
102
|
+
entities, channelData: { feedbackLoopEnabled: true },
|
|
103
|
+
};
|
|
104
|
+
if (Buffer.byteLength(JSON.stringify(message), "utf8") > 80 * 1024) {
|
|
105
|
+
throw new Error("Teams image message exceeds the inline payload budget; nothing was sent");
|
|
106
|
+
}
|
|
107
|
+
const receipt = await send(message);
|
|
108
|
+
if (!receipt || typeof receipt !== "object" || !("id" in receipt)
|
|
109
|
+
|| typeof receipt.id !== "string" || !receipt.id.trim()) {
|
|
110
|
+
throw new Error("Teams image delivery is unknown: no activity receipt returned; do not retry automatically");
|
|
111
|
+
}
|
|
112
|
+
(0, runtime_1.emitNervesEvent)({
|
|
113
|
+
component: "channels", event: "channel.teams_image_accepted",
|
|
114
|
+
message: "Teams accepted an inline image activity; client rendering is not verified",
|
|
115
|
+
meta: { activityId: receipt.id, mimeType: attachment.contentType },
|
|
116
|
+
});
|
|
117
|
+
return receipt.id;
|
|
118
|
+
};
|
|
119
|
+
}
|
package/dist/senses/teams.js
CHANGED
|
@@ -78,6 +78,7 @@ const session_transaction_1 = require("../mind/session-transaction");
|
|
|
78
78
|
const teamsFailoverStates = new Map();
|
|
79
79
|
const pending_1 = require("../mind/pending");
|
|
80
80
|
const continuity_1 = require("./continuity");
|
|
81
|
+
const teams_image_1 = require("./teams-image");
|
|
81
82
|
// AIGeneratedContent entity and feedbackLoopEnabled channelData for all outbound
|
|
82
83
|
// Teams messages. Required by Teams AI UX best practices.
|
|
83
84
|
function aiLabelEntities() {
|
|
@@ -721,6 +722,7 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
|
|
|
721
722
|
summarize: (0, core_1.createSummarize)("human"),
|
|
722
723
|
tenantId: teamsContext.tenantId,
|
|
723
724
|
botApi: teamsContext.botApi,
|
|
725
|
+
teamsSendImage: teamsContext.teamsSendImage,
|
|
724
726
|
} : {}) };
|
|
725
727
|
let currentText = text;
|
|
726
728
|
const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)(owner) ?? undefined;
|
|
@@ -1181,6 +1183,7 @@ function registerBotHandlers(app, label) {
|
|
|
1181
1183
|
githubConnectionName: tenantOAuth.githubConnectionName,
|
|
1182
1184
|
/* v8 ignore next -- bot API availability branch; requires live SDK context @preserve */
|
|
1183
1185
|
botApi: app.id && api ? { id: app.id, conversations: api.conversations } : undefined,
|
|
1186
|
+
teamsSendImage: (0, teams_image_1.createTeamsImageSender)((message) => ctx.send(message), activity.id, aiLabelEntities()),
|
|
1184
1187
|
};
|
|
1185
1188
|
/* v8 ignore next 5 -- bot-framework integration callback; tested via handleTeamsMessage sendMessage path @preserve */
|
|
1186
1189
|
const ctxSend = async (t) => {
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.929",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@ouro.bot/cli",
|
|
9
|
-
"version": "0.1.0-alpha.
|
|
9
|
+
"version": "0.1.0-alpha.929",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|