u-foo 1.9.7 → 2.1.0
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/bin/ufoo.js +5 -3
- package/package.json +2 -4
- package/src/agent/claudeEventTranslator.js +267 -0
- package/src/agent/claudeOauthTokenReader.js +52 -0
- package/src/agent/claudeThreadProvider.js +343 -0
- package/src/agent/cliRunner.js +10 -16
- package/src/agent/codexEventTranslator.js +78 -0
- package/src/agent/codexThreadProvider.js +181 -0
- package/src/agent/controllerToolExecutor.js +233 -0
- package/src/agent/credentials/claude.js +324 -0
- package/src/agent/credentials/codex.js +203 -0
- package/src/agent/credentials/index.js +106 -0
- package/src/agent/internalRunner.js +348 -3
- package/src/agent/loopObservability.js +190 -0
- package/src/agent/loopRuntime.js +457 -0
- package/src/agent/ptyRunner.js +8 -7
- package/src/agent/ufooAgent.js +178 -120
- package/src/agent/upstreamTransport.js +464 -0
- package/src/bus/utils.js +3 -2
- package/src/chat/dashboardView.js +51 -1
- package/src/chat/index.js +3 -1
- package/src/config.js +53 -17
- package/src/controller/flags.js +160 -0
- package/src/controller/gateRouter.js +201 -0
- package/src/controller/routerFastPath.js +22 -0
- package/src/controller/shadowGuard.js +280 -0
- package/src/daemon/index.js +2 -3
- package/src/daemon/promptLoop.js +33 -224
- package/src/daemon/promptRequest.js +360 -5
- package/src/daemon/status.js +2 -0
- package/src/history/inputTimeline.js +9 -4
- package/src/memory/index.js +24 -0
- package/src/providerapi/redactor.js +87 -0
- package/src/providerapi/shadowDiff.js +174 -0
- package/src/report/store.js +4 -3
- package/src/tools/handlers/ackBus.js +26 -0
- package/src/tools/handlers/common.js +64 -0
- package/src/tools/handlers/dispatchMessage.js +81 -0
- package/src/tools/handlers/listAgents.js +14 -0
- package/src/tools/handlers/readBusSummary.js +34 -0
- package/src/tools/handlers/readOpenDecisions.js +26 -0
- package/src/tools/handlers/readProjectRegistry.js +20 -0
- package/src/tools/handlers/readPromptHistory.js +123 -0
- package/src/tools/handlers/tier2.js +134 -0
- package/src/tools/index.js +55 -0
- package/src/tools/registry.js +69 -0
- package/src/tools/schemaFixtures.js +415 -0
- package/src/tools/tier0/listAgents.js +14 -0
- package/src/tools/tier0/readBusSummary.js +14 -0
- package/src/tools/tier0/readOpenDecisions.js +14 -0
- package/src/tools/tier0/readProjectRegistry.js +14 -0
- package/src/tools/tier0/readPromptHistory.js +14 -0
- package/src/tools/tier1/ackBus.js +14 -0
- package/src/tools/tier1/dispatchMessage.js +14 -0
- package/src/tools/tier1/routeAgent.js +14 -0
- package/src/tools/tier2/closeAgent.js +14 -0
- package/src/tools/tier2/launchAgent.js +14 -0
- package/src/tools/tier2/manageCron.js +14 -0
- package/src/tools/tier2/renameAgent.js +14 -0
- package/src/tools/types.js +75 -0
- package/src/tools/unimplemented.js +13 -0
- package/src/ufoo/paths.js +4 -0
- package/bin/ufoo-assistant-agent.js +0 -5
- package/bin/ufoo-engine.js +0 -25
- package/src/assistant/agent.js +0 -261
- package/src/assistant/bridge.js +0 -178
- package/src/assistant/constants.js +0 -15
- package/src/assistant/engine.js +0 -252
- package/src/assistant/stdio.js +0 -58
- package/src/assistant/ufooEngineCli.js +0 -312
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// §11.6 Phase 1 shadow diff metrics: model text BLEU ≥ 0.85 and
|
|
4
|
+
// tool-call sequence consistency ≥ 95%. This module provides the deterministic
|
|
5
|
+
// helpers the shadow harness feeds paired (legacy, api-backed) samples into.
|
|
6
|
+
|
|
7
|
+
const PHASE1_DEFAULT_BLEU_THRESHOLD = 0.85;
|
|
8
|
+
const PHASE1_DEFAULT_TOOLCALL_THRESHOLD = 0.95;
|
|
9
|
+
|
|
10
|
+
function tokenize(text) {
|
|
11
|
+
return String(text || "")
|
|
12
|
+
.toLowerCase()
|
|
13
|
+
.replace(/[^\p{L}\p{N}\s]/gu, " ")
|
|
14
|
+
.split(/\s+/)
|
|
15
|
+
.filter(Boolean);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ngrams(tokens, n) {
|
|
19
|
+
if (!Array.isArray(tokens) || tokens.length < n) return [];
|
|
20
|
+
const out = [];
|
|
21
|
+
for (let i = 0; i <= tokens.length - n; i += 1) {
|
|
22
|
+
out.push(tokens.slice(i, i + n).join("\u0001"));
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function countMap(list) {
|
|
28
|
+
const map = new Map();
|
|
29
|
+
for (const item of list) {
|
|
30
|
+
map.set(item, (map.get(item) || 0) + 1);
|
|
31
|
+
}
|
|
32
|
+
return map;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function clippedPrecision(candidateNgrams, referenceNgrams) {
|
|
36
|
+
if (candidateNgrams.length === 0) return 0;
|
|
37
|
+
const candidateCounts = countMap(candidateNgrams);
|
|
38
|
+
const referenceCounts = countMap(referenceNgrams);
|
|
39
|
+
let clipped = 0;
|
|
40
|
+
for (const [gram, count] of candidateCounts.entries()) {
|
|
41
|
+
const refCount = referenceCounts.get(gram) || 0;
|
|
42
|
+
clipped += Math.min(count, refCount);
|
|
43
|
+
}
|
|
44
|
+
return clipped / candidateNgrams.length;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function brevityPenalty(candidateLen, referenceLen) {
|
|
48
|
+
if (candidateLen === 0) return 0;
|
|
49
|
+
if (candidateLen >= referenceLen) return 1;
|
|
50
|
+
return Math.exp(1 - referenceLen / candidateLen);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function computeBleu(referenceText, candidateText, options = {}) {
|
|
54
|
+
const maxN = Number.isFinite(options.maxN) ? Math.max(1, Math.min(4, options.maxN)) : 4;
|
|
55
|
+
const weight = 1 / maxN;
|
|
56
|
+
const ref = tokenize(referenceText);
|
|
57
|
+
const cand = tokenize(candidateText);
|
|
58
|
+
if (cand.length === 0) return 0;
|
|
59
|
+
if (ref.length === 0) return 0;
|
|
60
|
+
|
|
61
|
+
let logSum = 0;
|
|
62
|
+
for (let n = 1; n <= maxN; n += 1) {
|
|
63
|
+
const precision = clippedPrecision(ngrams(cand, n), ngrams(ref, n));
|
|
64
|
+
if (precision <= 0) return 0;
|
|
65
|
+
logSum += weight * Math.log(precision);
|
|
66
|
+
}
|
|
67
|
+
const bp = brevityPenalty(cand.length, ref.length);
|
|
68
|
+
return bp * Math.exp(logSum);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function extractToolCallNames(events = []) {
|
|
72
|
+
return (Array.isArray(events) ? events : [])
|
|
73
|
+
.filter((event) => event && event.type === "tool_call" && event.name)
|
|
74
|
+
.map((event) => String(event.name).trim())
|
|
75
|
+
.filter(Boolean);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function computeToolCallSequenceConsistency(referenceSeq = [], candidateSeq = []) {
|
|
79
|
+
const ref = Array.isArray(referenceSeq) ? referenceSeq : [];
|
|
80
|
+
const cand = Array.isArray(candidateSeq) ? candidateSeq : [];
|
|
81
|
+
if (ref.length === 0 && cand.length === 0) return 1;
|
|
82
|
+
|
|
83
|
+
const m = ref.length;
|
|
84
|
+
const n = cand.length;
|
|
85
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
86
|
+
for (let i = 1; i <= m; i += 1) {
|
|
87
|
+
for (let j = 1; j <= n; j += 1) {
|
|
88
|
+
if (ref[i - 1] === cand[j - 1]) {
|
|
89
|
+
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
90
|
+
} else {
|
|
91
|
+
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const lcs = dp[m][n];
|
|
96
|
+
const denom = Math.max(m, n);
|
|
97
|
+
if (denom === 0) return 1;
|
|
98
|
+
return lcs / denom;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function buildPhase1ShadowDiffSample({ legacy = {}, api = {} } = {}) {
|
|
102
|
+
const bleu = computeBleu(legacy.text || "", api.text || "");
|
|
103
|
+
const toolSeqConsistency = computeToolCallSequenceConsistency(
|
|
104
|
+
extractToolCallNames(legacy.events || legacy.toolCalls || []),
|
|
105
|
+
extractToolCallNames(api.events || api.toolCalls || [])
|
|
106
|
+
);
|
|
107
|
+
return {
|
|
108
|
+
bleu,
|
|
109
|
+
toolSeqConsistency,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function summarizePhase1ShadowDiff(samples = [], options = {}) {
|
|
114
|
+
const bleuThreshold = Number.isFinite(options.bleuThreshold)
|
|
115
|
+
? options.bleuThreshold
|
|
116
|
+
: PHASE1_DEFAULT_BLEU_THRESHOLD;
|
|
117
|
+
const toolCallThreshold = Number.isFinite(options.toolCallThreshold)
|
|
118
|
+
? options.toolCallThreshold
|
|
119
|
+
: PHASE1_DEFAULT_TOOLCALL_THRESHOLD;
|
|
120
|
+
|
|
121
|
+
const list = Array.isArray(samples) ? samples : [];
|
|
122
|
+
if (list.length === 0) {
|
|
123
|
+
return {
|
|
124
|
+
sampleCount: 0,
|
|
125
|
+
meanBleu: 0,
|
|
126
|
+
meanToolSeqConsistency: 0,
|
|
127
|
+
bleuPass: false,
|
|
128
|
+
toolCallPass: false,
|
|
129
|
+
overallPass: false,
|
|
130
|
+
bleuThreshold,
|
|
131
|
+
toolCallThreshold,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let totalBleu = 0;
|
|
136
|
+
let totalTool = 0;
|
|
137
|
+
let toolCallPassCount = 0;
|
|
138
|
+
for (const sample of list) {
|
|
139
|
+
totalBleu += Number(sample.bleu || 0);
|
|
140
|
+
const consistency = Number(sample.toolSeqConsistency || 0);
|
|
141
|
+
totalTool += consistency;
|
|
142
|
+
if (consistency >= toolCallThreshold) toolCallPassCount += 1;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const meanBleu = totalBleu / list.length;
|
|
146
|
+
const meanToolSeqConsistency = totalTool / list.length;
|
|
147
|
+
const bleuPass = meanBleu >= bleuThreshold;
|
|
148
|
+
const toolCallPassRate = toolCallPassCount / list.length;
|
|
149
|
+
const toolCallPass = toolCallPassRate >= toolCallThreshold;
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
sampleCount: list.length,
|
|
153
|
+
meanBleu,
|
|
154
|
+
meanToolSeqConsistency,
|
|
155
|
+
toolCallPassRate,
|
|
156
|
+
bleuPass,
|
|
157
|
+
toolCallPass,
|
|
158
|
+
overallPass: bleuPass && toolCallPass,
|
|
159
|
+
bleuThreshold,
|
|
160
|
+
toolCallThreshold,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = {
|
|
165
|
+
PHASE1_DEFAULT_BLEU_THRESHOLD,
|
|
166
|
+
PHASE1_DEFAULT_TOOLCALL_THRESHOLD,
|
|
167
|
+
tokenize,
|
|
168
|
+
ngrams,
|
|
169
|
+
computeBleu,
|
|
170
|
+
extractToolCallNames,
|
|
171
|
+
computeToolCallSequenceConsistency,
|
|
172
|
+
buildPhase1ShadowDiffSample,
|
|
173
|
+
summarizePhase1ShadowDiff,
|
|
174
|
+
};
|
package/src/report/store.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const { getUfooPaths } = require("../ufoo/paths");
|
|
4
|
+
const { redactSecrets } = require("../providerapi/redactor");
|
|
4
5
|
|
|
5
6
|
const REPORT_PHASES = {
|
|
6
7
|
START: "start",
|
|
@@ -100,7 +101,7 @@ function normalizeReportInput(input = {}, options = {}) {
|
|
|
100
101
|
function appendReport(projectRoot, entry) {
|
|
101
102
|
ensureReportDir(projectRoot);
|
|
102
103
|
const { reportsFile } = getReportPaths(projectRoot);
|
|
103
|
-
fs.appendFileSync(reportsFile, `${JSON.stringify(entry)}\n`, "utf8");
|
|
104
|
+
fs.appendFileSync(reportsFile, `${JSON.stringify(redactSecrets(entry))}\n`, "utf8");
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
function parseJsonLines(file) {
|
|
@@ -151,7 +152,7 @@ function loadReportState(projectRoot) {
|
|
|
151
152
|
function saveReportState(projectRoot, state) {
|
|
152
153
|
ensureReportDir(projectRoot);
|
|
153
154
|
const { stateFile } = getReportPaths(projectRoot);
|
|
154
|
-
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
|
|
155
|
+
fs.writeFileSync(stateFile, JSON.stringify(redactSecrets(state), null, 2));
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
function updateReportState(projectRoot, entry) {
|
|
@@ -205,7 +206,7 @@ function getControllerInboxFile(projectRoot, controllerId = "ufoo-agent") {
|
|
|
205
206
|
function appendControllerInboxEntry(projectRoot, controllerId, entry) {
|
|
206
207
|
const file = getControllerInboxFile(projectRoot, controllerId);
|
|
207
208
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
208
|
-
fs.appendFileSync(file, `${JSON.stringify(entry)}\n`, "utf8");
|
|
209
|
+
fs.appendFileSync(file, `${JSON.stringify(redactSecrets(entry))}\n`, "utf8");
|
|
209
210
|
}
|
|
210
211
|
|
|
211
212
|
function listControllerInboxEntries(projectRoot, controllerId = "ufoo-agent", options = {}) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const { buildToolError, requireSubscriber, getEventBus } = require("./common");
|
|
2
|
+
|
|
3
|
+
async function ackBusHandler(ctx = {}, args = {}) {
|
|
4
|
+
const subscriber = requireSubscriber(ctx);
|
|
5
|
+
const requestedSubscriber = String(args.subscriber || subscriber).trim();
|
|
6
|
+
|
|
7
|
+
if (requestedSubscriber !== subscriber) {
|
|
8
|
+
throw buildToolError(
|
|
9
|
+
"forbidden_ack",
|
|
10
|
+
"ack_bus can only acknowledge the caller subscriber queue"
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const eventBus = getEventBus(ctx);
|
|
15
|
+
const count = await eventBus.ack(subscriber);
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
ok: true,
|
|
19
|
+
subscriber,
|
|
20
|
+
acknowledged: count,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
ackBusHandler,
|
|
26
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const EventBus = require("../../bus");
|
|
2
|
+
const { CALLER_TIERS, normalizeCallerTier } = require("../types");
|
|
3
|
+
|
|
4
|
+
function extractAuditFields(ctx = {}) {
|
|
5
|
+
const audit = {};
|
|
6
|
+
const turnId = ctx.turn_id || ctx.turnId;
|
|
7
|
+
const toolCallId = ctx.tool_call_id || ctx.toolCallId;
|
|
8
|
+
if (turnId) audit.turn_id = String(turnId);
|
|
9
|
+
if (toolCallId) audit.tool_call_id = String(toolCallId);
|
|
10
|
+
const callerTier = normalizeCallerTier(ctx.caller_tier || ctx.callerTier);
|
|
11
|
+
if (callerTier) audit.caller_tier = callerTier;
|
|
12
|
+
return audit;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function buildToolError(code, message, extra = {}) {
|
|
16
|
+
const err = new Error(String(message || "tool execution failed"));
|
|
17
|
+
err.code = String(code || "tool_error");
|
|
18
|
+
Object.assign(err, extra);
|
|
19
|
+
return err;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function requireSubscriber(ctx = {}) {
|
|
23
|
+
const subscriber = String(ctx.subscriber || "").trim();
|
|
24
|
+
if (!subscriber) {
|
|
25
|
+
throw buildToolError("invalid_context", "tool requires subscriber context", extractAuditFields(ctx));
|
|
26
|
+
}
|
|
27
|
+
return subscriber;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getEventBus(ctx = {}) {
|
|
31
|
+
if (ctx.eventBus) return ctx.eventBus;
|
|
32
|
+
return new EventBus(ctx.projectRoot);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveCallerTier(ctx = {}) {
|
|
36
|
+
const raw = normalizeCallerTier(ctx.caller_tier || ctx.callerTier);
|
|
37
|
+
return raw || CALLER_TIERS.CONTROLLER;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function assertControllerTier(ctx = {}, toolName = "") {
|
|
41
|
+
const tier = resolveCallerTier(ctx);
|
|
42
|
+
if (tier !== CALLER_TIERS.CONTROLLER) {
|
|
43
|
+
const audit = extractAuditFields(ctx);
|
|
44
|
+
audit.caller_tier = tier;
|
|
45
|
+
throw buildToolError(
|
|
46
|
+
"forbidden_caller_tier",
|
|
47
|
+
`caller_tier "${tier}" is not allowed to invoke tool "${toolName}"`,
|
|
48
|
+
{
|
|
49
|
+
tool_name: toolName,
|
|
50
|
+
allowed_tiers: [CALLER_TIERS.CONTROLLER],
|
|
51
|
+
...audit,
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
buildToolError,
|
|
59
|
+
requireSubscriber,
|
|
60
|
+
getEventBus,
|
|
61
|
+
resolveCallerTier,
|
|
62
|
+
assertControllerTier,
|
|
63
|
+
extractAuditFields,
|
|
64
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const { buildToolError, requireSubscriber, getEventBus } = require("./common");
|
|
2
|
+
|
|
3
|
+
function normalizeDispatchTarget(rawTarget = "") {
|
|
4
|
+
const target = String(rawTarget || "").trim();
|
|
5
|
+
if (!target) {
|
|
6
|
+
throw buildToolError("invalid_arguments", "dispatch_message requires target");
|
|
7
|
+
}
|
|
8
|
+
if (target === "*") return "broadcast";
|
|
9
|
+
return target;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function normalizeDispatchMode(args = {}) {
|
|
13
|
+
const raw = String(
|
|
14
|
+
args.mode || args.injection_mode || args.injectionMode || "immediate"
|
|
15
|
+
)
|
|
16
|
+
.trim()
|
|
17
|
+
.toLowerCase();
|
|
18
|
+
if (raw === "queued") return "queued";
|
|
19
|
+
if (raw === "immediate") return "immediate";
|
|
20
|
+
throw buildToolError(
|
|
21
|
+
"invalid_arguments",
|
|
22
|
+
"dispatch_message mode must be immediate or queued"
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function dispatchMessageHandler(ctx = {}, args = {}) {
|
|
27
|
+
const subscriber = requireSubscriber(ctx);
|
|
28
|
+
const target = normalizeDispatchTarget(args.target);
|
|
29
|
+
const message = String(args.message || "").trim();
|
|
30
|
+
const source = String(args.source || subscriber).trim();
|
|
31
|
+
const mode = normalizeDispatchMode(args);
|
|
32
|
+
|
|
33
|
+
if (!message) {
|
|
34
|
+
throw buildToolError("invalid_arguments", "dispatch_message requires message");
|
|
35
|
+
}
|
|
36
|
+
if (source !== subscriber) {
|
|
37
|
+
throw buildToolError(
|
|
38
|
+
"forbidden_source",
|
|
39
|
+
"dispatch_message source must match caller subscriber"
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const eventBus = getEventBus(ctx);
|
|
44
|
+
let result;
|
|
45
|
+
try {
|
|
46
|
+
result = target === "broadcast"
|
|
47
|
+
? await eventBus.broadcast(message, subscriber, {
|
|
48
|
+
injectionMode: mode,
|
|
49
|
+
source: subscriber,
|
|
50
|
+
silent: true,
|
|
51
|
+
})
|
|
52
|
+
: await eventBus.send(target, message, subscriber, {
|
|
53
|
+
injectionMode: mode,
|
|
54
|
+
source: subscriber,
|
|
55
|
+
silent: true,
|
|
56
|
+
});
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if (err && /not found/i.test(String(err.message || ""))) {
|
|
59
|
+
throw buildToolError(
|
|
60
|
+
"invalid_target",
|
|
61
|
+
`dispatch_message target not found: ${target}`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
ok: true,
|
|
69
|
+
target,
|
|
70
|
+
source: subscriber,
|
|
71
|
+
mode,
|
|
72
|
+
delivered: mode === "immediate" ? result.targets.length : 0,
|
|
73
|
+
queued: mode === "queued" ? result.targets.length : 0,
|
|
74
|
+
targets: result.targets,
|
|
75
|
+
seq: result.seq,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
dispatchMessageHandler,
|
|
81
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const { buildStatus } = require("../../daemon/status");
|
|
2
|
+
|
|
3
|
+
function listAgentsHandler(ctx = {}) {
|
|
4
|
+
const status = buildStatus(ctx.projectRoot);
|
|
5
|
+
const agents = Array.isArray(status.active_meta) ? status.active_meta : [];
|
|
6
|
+
return {
|
|
7
|
+
count: agents.length,
|
|
8
|
+
agents,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
listAgentsHandler,
|
|
14
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const { buildStatus } = require("../../daemon/status");
|
|
2
|
+
|
|
3
|
+
function readBusSummaryHandler(ctx = {}) {
|
|
4
|
+
const status = buildStatus(ctx.projectRoot);
|
|
5
|
+
const activeAgents = Array.isArray(status.active_meta) ? status.active_meta : [];
|
|
6
|
+
const busyCount = activeAgents.filter((item) => {
|
|
7
|
+
const state = String((item && item.activity_state) || "").trim().toLowerCase();
|
|
8
|
+
return state === "working"
|
|
9
|
+
|| state === "starting"
|
|
10
|
+
|| state === "running"
|
|
11
|
+
|| state === "waiting_input"
|
|
12
|
+
|| state === "blocked";
|
|
13
|
+
}).length;
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
project_root: ctx.projectRoot,
|
|
17
|
+
summary: {
|
|
18
|
+
active_count: activeAgents.length,
|
|
19
|
+
busy_count: busyCount,
|
|
20
|
+
ready_count: Math.max(activeAgents.length - busyCount, 0),
|
|
21
|
+
unread_total: Number(status.unread && status.unread.total ? status.unread.total : 0) || 0,
|
|
22
|
+
decisions_open: Number(status.decisions && status.decisions.open ? status.decisions.open : 0) || 0,
|
|
23
|
+
reports_pending_total: Number(status.reports && status.reports.pending_total ? status.reports.pending_total : 0) || 0,
|
|
24
|
+
controller_pending_total: Number(status.controller && status.controller.pending_total ? status.controller.pending_total : 0) || 0,
|
|
25
|
+
cron_count: Number(status.cron && status.cron.count ? status.cron.count : 0) || 0,
|
|
26
|
+
groups_active: Number(status.groups && status.groups.active ? status.groups.active : 0) || 0,
|
|
27
|
+
},
|
|
28
|
+
active_agents: activeAgents,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = {
|
|
33
|
+
readBusSummaryHandler,
|
|
34
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const DecisionsManager = require("../../context/decisions");
|
|
2
|
+
|
|
3
|
+
function readOpenDecisionsHandler(ctx = {}, args = {}) {
|
|
4
|
+
const limit = Number.isFinite(Number(args.limit)) && Number(args.limit) > 0
|
|
5
|
+
? Math.floor(Number(args.limit))
|
|
6
|
+
: 20;
|
|
7
|
+
const manager = new DecisionsManager(ctx.projectRoot);
|
|
8
|
+
const decisions = manager.readDecisions()
|
|
9
|
+
.filter((item) => String(item.status || "open").trim().toLowerCase() === "open")
|
|
10
|
+
.slice(0, limit)
|
|
11
|
+
.map((item) => ({
|
|
12
|
+
file: item.file,
|
|
13
|
+
title: item.title,
|
|
14
|
+
status: item.status,
|
|
15
|
+
file_path: item.filePath,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
count: decisions.length,
|
|
20
|
+
decisions,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
readOpenDecisionsHandler,
|
|
26
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const { listProjectRuntimes } = require("../../projects/registry");
|
|
2
|
+
|
|
3
|
+
function readProjectRegistryHandler(_ctx = {}, args = {}) {
|
|
4
|
+
const validate = args.validate !== false;
|
|
5
|
+
const cleanupTmp = args.cleanup_tmp !== false;
|
|
6
|
+
const projects = listProjectRuntimes({
|
|
7
|
+
validate,
|
|
8
|
+
cleanupTmp,
|
|
9
|
+
runtimeDir: args.runtimeDir,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
count: projects.length,
|
|
14
|
+
projects,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
readProjectRegistryHandler,
|
|
20
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const { buildStatus } = require("../../daemon/status");
|
|
5
|
+
const { getUfooPaths } = require("../../ufoo/paths");
|
|
6
|
+
|
|
7
|
+
function clipPromptText(value = "", maxChars = 240) {
|
|
8
|
+
const text = String(value || "").replace(/\s+/g, " ").trim();
|
|
9
|
+
if (!text) return "";
|
|
10
|
+
if (text.length <= maxChars) return text;
|
|
11
|
+
return `${text.slice(0, maxChars)}...[truncated]`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function buildPromptHistory(projectRoot, activeAgents = [], args = {}) {
|
|
15
|
+
const perAgentLimit = Number.isFinite(Number(args.per_agent_limit)) && Number(args.per_agent_limit) > 0
|
|
16
|
+
? Math.floor(Number(args.per_agent_limit))
|
|
17
|
+
: 6;
|
|
18
|
+
const maxFiles = Number.isFinite(Number(args.max_files)) && Number(args.max_files) > 0
|
|
19
|
+
? Math.floor(Number(args.max_files))
|
|
20
|
+
: 3;
|
|
21
|
+
const target = String(args.target || "").trim();
|
|
22
|
+
const eventsDir = getUfooPaths(projectRoot).busEventsDir;
|
|
23
|
+
const activeIds = new Set(activeAgents.map((item) => String(item.id || "")).filter(Boolean));
|
|
24
|
+
const nicknames = {};
|
|
25
|
+
const rows = new Map();
|
|
26
|
+
|
|
27
|
+
for (const item of activeAgents) {
|
|
28
|
+
if (!item || !item.id) continue;
|
|
29
|
+
const id = String(item.id);
|
|
30
|
+
const nickname = String(item.nickname || "");
|
|
31
|
+
if (nickname) nicknames[nickname] = id;
|
|
32
|
+
rows.set(id, {
|
|
33
|
+
agent_id: id,
|
|
34
|
+
nickname,
|
|
35
|
+
samples: [],
|
|
36
|
+
sample_count: 0,
|
|
37
|
+
total_count: 0,
|
|
38
|
+
last_ts: "",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (target) {
|
|
43
|
+
const resolved = activeIds.has(target) ? target : nicknames[target];
|
|
44
|
+
if (resolved) {
|
|
45
|
+
for (const id of Array.from(rows.keys())) {
|
|
46
|
+
if (id !== resolved) rows.delete(id);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let files = [];
|
|
52
|
+
try {
|
|
53
|
+
files = fs.readdirSync(eventsDir)
|
|
54
|
+
.filter((name) => name.endsWith(".jsonl"))
|
|
55
|
+
.sort()
|
|
56
|
+
.slice(-maxFiles)
|
|
57
|
+
.reverse();
|
|
58
|
+
} catch {
|
|
59
|
+
return { scanned_files: 0, matched_events: 0, per_agent: [] };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let matchedEvents = 0;
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
let lines = [];
|
|
65
|
+
try {
|
|
66
|
+
lines = fs.readFileSync(path.join(eventsDir, file), "utf8")
|
|
67
|
+
.split(/\r?\n/)
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
.reverse();
|
|
70
|
+
} catch {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const line of lines) {
|
|
75
|
+
let event = null;
|
|
76
|
+
try {
|
|
77
|
+
event = JSON.parse(line);
|
|
78
|
+
} catch {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (!event || event.event !== "message") continue;
|
|
82
|
+
const message = clipPromptText(event.data && event.data.message);
|
|
83
|
+
if (!message) continue;
|
|
84
|
+
const rawTarget = String(event.target || "").trim();
|
|
85
|
+
const agentId = rows.has(rawTarget) ? rawTarget : nicknames[rawTarget];
|
|
86
|
+
if (!agentId || !rows.has(agentId)) continue;
|
|
87
|
+
|
|
88
|
+
const row = rows.get(agentId);
|
|
89
|
+
matchedEvents += 1;
|
|
90
|
+
row.total_count += 1;
|
|
91
|
+
if (!row.last_ts) row.last_ts = String(event.timestamp || "");
|
|
92
|
+
if (row.samples.length < perAgentLimit) {
|
|
93
|
+
row.samples.push({
|
|
94
|
+
ts: String(event.timestamp || ""),
|
|
95
|
+
publisher: String(event.publisher || ""),
|
|
96
|
+
prompt: message,
|
|
97
|
+
});
|
|
98
|
+
row.sample_count = row.samples.length;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const perAgent = Array.from(rows.values())
|
|
104
|
+
.filter((row) => row.total_count > 0)
|
|
105
|
+
.sort((a, b) => String(b.last_ts || "").localeCompare(String(a.last_ts || "")));
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
scanned_files: files.length,
|
|
109
|
+
matched_events: matchedEvents,
|
|
110
|
+
per_agent: perAgent,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function readPromptHistoryHandler(ctx = {}, args = {}) {
|
|
115
|
+
const status = buildStatus(ctx.projectRoot);
|
|
116
|
+
const activeAgents = Array.isArray(status.active_meta) ? status.active_meta : [];
|
|
117
|
+
return buildPromptHistory(ctx.projectRoot, activeAgents, args);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
buildPromptHistory,
|
|
122
|
+
readPromptHistoryHandler,
|
|
123
|
+
};
|