takomi 2.1.43 → 2.1.45
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/.pi/README.md +4 -3
- package/.pi/extensions/oauth-router/README.md +3 -3
- package/.pi/extensions/oauth-router/commands.ts +66 -39
- package/.pi/extensions/oauth-router/config.ts +34 -34
- package/.pi/extensions/oauth-router/index.ts +51 -10
- package/.pi/extensions/oauth-router/report-ui.ts +205 -0
- package/.pi/extensions/oauth-router/scripts/vibe-verify.py +5 -5
- package/.pi/extensions/takomi-context-manager/diagnostics-tools.ts +41 -3
- package/.pi/extensions/takomi-context-manager/diagnostics.ts +26 -0
- package/.pi/extensions/takomi-context-manager/index.ts +20 -9
- package/.pi/extensions/takomi-context-manager/model-policy-gate.ts +36 -15
- package/.pi/extensions/takomi-context-manager/policy-tools.ts +93 -42
- package/.pi/extensions/takomi-context-manager/skill-categories.d.ts +11 -0
- package/.pi/extensions/takomi-context-manager/skill-categories.js +212 -0
- package/.pi/extensions/takomi-context-manager/skill-registry.ts +179 -8
- package/.pi/extensions/takomi-context-manager/skill-tools.ts +208 -103
- package/.pi/extensions/takomi-context-manager/tool-renderers.ts +112 -0
- package/.pi/extensions/takomi-context-manager/types.ts +6 -0
- package/.pi/extensions/takomi-runtime/commands.ts +45 -12
- package/.pi/extensions/takomi-runtime/gate-provenance.ts +24 -0
- package/.pi/extensions/takomi-runtime/index.ts +133 -56
- package/.pi/extensions/takomi-runtime/routing-policy.ts +14 -2
- package/.pi/extensions/takomi-runtime/takomi-stats.js +46 -26
- package/.pi/extensions/takomi-runtime/tool-renderers.ts +234 -0
- package/.pi/extensions/takomi-runtime/workflow-catalog.ts +54 -0
- package/.pi/extensions/takomi-subagents/agents.ts +4 -0
- package/.pi/extensions/takomi-subagents/async-lifecycle.ts +401 -0
- package/.pi/extensions/takomi-subagents/detached-results.ts +940 -0
- package/.pi/extensions/takomi-subagents/index.ts +46 -1
- package/.pi/extensions/takomi-subagents/native-render.ts +250 -188
- package/.pi/extensions/takomi-subagents/pi-subagents-engine.ts +55 -46
- package/.pi/extensions/takomi-subagents/pi-subagents-internal.ts +11 -5
- package/.pi/extensions/takomi-subagents/result-heartbeat.ts +55 -0
- package/.pi/extensions/takomi-subagents/subagent-ux.ts +162 -0
- package/.pi/extensions/takomi-subagents/tool-runner.ts +158 -28
- package/.pi/takomi/model-routing.md +282 -3
- package/assets/.agent/skills/shared-resend-portfolio/SKILL.md +124 -0
- package/package.json +4 -3
- package/src/pi-harness.js +41 -1
- package/src/pi-takomi-core/types.ts +10 -0
- package/src/pi-takomi-core/workflows.ts +86 -45
- package/src/skills-catalog.js +2 -202
- package/src/skills-installer.js +4 -1
- package/src/takomi-stats.js +5 -5
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { resolveAgentName } from "./agent-aliases";
|
|
15
15
|
import { applyTakomiRoutingDefaults, loadTakomiModelRoutingSnapshotSync } from "../takomi-runtime/model-routing-defaults";
|
|
16
16
|
import type { TakomiSubagentToolParams, TakomiSubagentToolTask } from "./tool-runner";
|
|
17
|
+
import { ensureTakomiAsyncLifecycle, getTakomiAsyncLifecycleSnapshot } from "./async-lifecycle";
|
|
17
18
|
|
|
18
19
|
type ToolUpdate = (partial: AgentToolResult<Details>) => void;
|
|
19
20
|
|
|
@@ -32,28 +33,6 @@ function expandTilde(value: string): string {
|
|
|
32
33
|
return value.startsWith("~/") ? path.join(os.homedir(), value.slice(2)) : value;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
function createState(): SubagentState {
|
|
36
|
-
return {
|
|
37
|
-
baseCwd: process.cwd(),
|
|
38
|
-
currentSessionId: null,
|
|
39
|
-
asyncJobs: new Map(),
|
|
40
|
-
foregroundRuns: new Map(),
|
|
41
|
-
foregroundControls: new Map(),
|
|
42
|
-
lastForegroundControlId: null,
|
|
43
|
-
pendingForegroundControlNotices: new Map(),
|
|
44
|
-
cleanupTimers: new Map(),
|
|
45
|
-
lastUiContext: null,
|
|
46
|
-
poller: null,
|
|
47
|
-
completionSeen: new Map(),
|
|
48
|
-
watcher: null,
|
|
49
|
-
watcherRestartTimer: null,
|
|
50
|
-
resultFileCoalescer: {
|
|
51
|
-
schedule: () => false,
|
|
52
|
-
clear: () => {},
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
36
|
function resolveMode(params: TakomiSubagentToolParams): "single" | "parallel" | "chain" | "action" | undefined {
|
|
58
37
|
if (params.action) return "action";
|
|
59
38
|
const hasChain = Boolean(params.chain?.length);
|
|
@@ -78,6 +57,7 @@ function resolveTasks(params: TakomiSubagentToolParams): TakomiSubagentToolTask[
|
|
|
78
57
|
conversationId: params.conversationId,
|
|
79
58
|
cwd: undefined,
|
|
80
59
|
checklist: params.checklist,
|
|
60
|
+
acceptance: params.acceptance,
|
|
81
61
|
}];
|
|
82
62
|
}
|
|
83
63
|
return [];
|
|
@@ -92,6 +72,7 @@ function buildTakomiTaskPrompt(task: TakomiSubagentToolTask): string {
|
|
|
92
72
|
? [
|
|
93
73
|
"Checklist:",
|
|
94
74
|
...task.checklist.map((item) => typeof item === "string" ? `- [ ] ${item}` : `- [${item.done ? "x" : " "}] ${item.text}`),
|
|
75
|
+
"When an item's state changes, report that exact item in explicit assistant progress/final output as a markdown checkbox. Mark it complete only after it is actually complete.",
|
|
95
76
|
].join("\n")
|
|
96
77
|
: "";
|
|
97
78
|
const takomiContext = [
|
|
@@ -194,6 +175,10 @@ function mapSingleTask(task: TakomiSubagentToolTask, names: Set<string>, rootCwd
|
|
|
194
175
|
model: modelWithThinking(task.model, task.thinking),
|
|
195
176
|
fallbackModels: task.fallbackModels,
|
|
196
177
|
skill: task.skills?.length ? task.skills : undefined,
|
|
178
|
+
// Native omission means auto-inferred enforcement. Takomi's public default
|
|
179
|
+
// is deliberately ordinary/no-contract; only caller-supplied acceptance is
|
|
180
|
+
// enforced, and every explicit value is forwarded unchanged.
|
|
181
|
+
acceptance: task.acceptance ?? { level: "none", reason: "No explicit Takomi acceptance contract." },
|
|
197
182
|
};
|
|
198
183
|
}
|
|
199
184
|
|
|
@@ -238,6 +223,7 @@ function toSubagentParams(params: TakomiSubagentToolParams, rootCwd: string, dis
|
|
|
238
223
|
model: mapped.model,
|
|
239
224
|
fallbackModels: mapped.fallbackModels,
|
|
240
225
|
skill: mapped.skill,
|
|
226
|
+
acceptance: mapped.acceptance,
|
|
241
227
|
};
|
|
242
228
|
}
|
|
243
229
|
|
|
@@ -259,42 +245,65 @@ function toSubagentParams(params: TakomiSubagentToolParams, rootCwd: string, dis
|
|
|
259
245
|
model: mapped.model,
|
|
260
246
|
fallbackModels: mapped.fallbackModels,
|
|
261
247
|
skill: mapped.skill,
|
|
248
|
+
acceptance: mapped.acceptance,
|
|
262
249
|
};
|
|
263
250
|
}),
|
|
264
251
|
};
|
|
265
252
|
}
|
|
266
253
|
|
|
267
254
|
export function createTakomiPiSubagentsEngine(pi: ExtensionAPI) {
|
|
268
|
-
|
|
269
|
-
|
|
255
|
+
let executorBinding: {
|
|
256
|
+
state: SubagentState;
|
|
257
|
+
generation: number;
|
|
258
|
+
promise: Promise<any>;
|
|
259
|
+
} | null = null;
|
|
270
260
|
|
|
271
|
-
async function getExecutor() {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
261
|
+
async function getExecutor(ctx: ExtensionContext) {
|
|
262
|
+
// Ownership can change when either extension reloads. Re-check after the
|
|
263
|
+
// executor factory resolves so an in-flight native takeover can never return
|
|
264
|
+
// an executor bound to a lifecycle that was disposed during initialization.
|
|
265
|
+
for (;;) {
|
|
266
|
+
const lifecycle = await ensureTakomiAsyncLifecycle(pi, ctx);
|
|
267
|
+
if (!executorBinding
|
|
268
|
+
|| executorBinding.state !== lifecycle.state
|
|
269
|
+
|| executorBinding.generation !== lifecycle.generation) {
|
|
270
|
+
const state = lifecycle.state;
|
|
271
|
+
executorBinding = {
|
|
272
|
+
state,
|
|
273
|
+
generation: lifecycle.generation,
|
|
274
|
+
promise: loadPiSubagentsInternals().then((internals) => {
|
|
275
|
+
const config = {
|
|
276
|
+
maxSubagentDepth: 2,
|
|
277
|
+
asyncByDefault: false,
|
|
278
|
+
forceTopLevelAsync: false,
|
|
279
|
+
};
|
|
280
|
+
return {
|
|
281
|
+
executor: internals.createSubagentExecutor({
|
|
282
|
+
pi,
|
|
283
|
+
state,
|
|
284
|
+
config,
|
|
285
|
+
asyncByDefault: false,
|
|
286
|
+
tempArtifactsDir: internals.TEMP_ARTIFACTS_DIR,
|
|
287
|
+
getSubagentSessionRoot,
|
|
288
|
+
expandTilde,
|
|
289
|
+
discoverAgents: (cwd: string, scope: AgentScope) => discoverUnifiedAgents(internals.discoverPiAgents, cwd, scope),
|
|
290
|
+
}),
|
|
291
|
+
discoverPiAgents: internals.discoverPiAgents,
|
|
292
|
+
};
|
|
289
293
|
}),
|
|
290
|
-
discoverPiAgents: internals.discoverPiAgents,
|
|
291
294
|
};
|
|
292
|
-
}
|
|
295
|
+
}
|
|
296
|
+
const binding = executorBinding;
|
|
297
|
+
const executor = await binding.promise;
|
|
298
|
+
const current = getTakomiAsyncLifecycleSnapshot(pi);
|
|
299
|
+
if (current?.state === binding.state && current?.generation === binding.generation) return executor;
|
|
293
300
|
}
|
|
294
|
-
return executorPromise;
|
|
295
301
|
}
|
|
296
302
|
|
|
297
303
|
return {
|
|
304
|
+
dispose(): void {
|
|
305
|
+
executorBinding = null;
|
|
306
|
+
},
|
|
298
307
|
async execute(
|
|
299
308
|
id: string,
|
|
300
309
|
params: TakomiSubagentToolParams,
|
|
@@ -303,7 +312,7 @@ export function createTakomiPiSubagentsEngine(pi: ExtensionAPI) {
|
|
|
303
312
|
ctx: ExtensionContext,
|
|
304
313
|
): Promise<AgentToolResult<Details>> {
|
|
305
314
|
const rootCwd = resolveRelativeCwd(ctx.cwd, params.cwd, "cwd");
|
|
306
|
-
const { executor, discoverPiAgents } = await getExecutor();
|
|
315
|
+
const { executor, discoverPiAgents } = await getExecutor(ctx);
|
|
307
316
|
const subagentParams = toSubagentParams(params, rootCwd, discoverPiAgents);
|
|
308
317
|
return executor.execute(id, subagentParams, signal ?? NEVER_ABORT, onUpdate, ctx);
|
|
309
318
|
},
|
|
@@ -14,28 +14,34 @@ let cachedInternals: any | null = null;
|
|
|
14
14
|
export async function loadPiSubagentsInternals() {
|
|
15
15
|
if (cachedInternals) return cachedInternals;
|
|
16
16
|
|
|
17
|
-
const [executorModule, agentsModule, sharedTypesModule, renderModule] = await Promise.all([
|
|
17
|
+
const [executorModule, agentsModule, sharedTypesModule, renderModule, watcherModule, sessionModule] = await Promise.all([
|
|
18
18
|
dynamicImport(spec("src/runs/foreground/subagent-executor")),
|
|
19
19
|
dynamicImport(spec("src/agents/agents")),
|
|
20
20
|
dynamicImport(spec("src/shared/types")),
|
|
21
21
|
dynamicImport(spec("src/tui/render")),
|
|
22
|
+
dynamicImport(spec("src/runs/background/result-watcher")),
|
|
23
|
+
dynamicImport(spec("src/shared/session-identity")),
|
|
22
24
|
]);
|
|
23
25
|
|
|
24
26
|
cachedInternals = {
|
|
25
27
|
createSubagentExecutor: executorModule.createSubagentExecutor,
|
|
28
|
+
createResultWatcher: watcherModule.createResultWatcher,
|
|
26
29
|
discoverPiAgents: agentsModule.discoverAgents,
|
|
30
|
+
resolveCurrentSessionId: sessionModule.resolveCurrentSessionId,
|
|
27
31
|
DEFAULT_ARTIFACT_CONFIG: sharedTypesModule.DEFAULT_ARTIFACT_CONFIG,
|
|
32
|
+
ASYNC_DIR: sharedTypesModule.ASYNC_DIR,
|
|
33
|
+
RESULTS_DIR: sharedTypesModule.RESULTS_DIR,
|
|
28
34
|
TEMP_ARTIFACTS_DIR: sharedTypesModule.TEMP_ARTIFACTS_DIR,
|
|
35
|
+
WIDGET_KEY: sharedTypesModule.WIDGET_KEY,
|
|
29
36
|
renderSubagentResult: renderModule.renderSubagentResult,
|
|
30
|
-
|
|
37
|
+
renderWidget: renderModule.renderWidget,
|
|
31
38
|
};
|
|
32
39
|
return cachedInternals;
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
export function renderNativeSubagentResult(result: unknown, options: unknown, theme: unknown,
|
|
42
|
+
export function renderNativeSubagentResult(result: unknown, options: unknown, theme: unknown, frame?: number): unknown | undefined {
|
|
36
43
|
if (!cachedInternals?.renderSubagentResult) return undefined;
|
|
37
|
-
cachedInternals.
|
|
38
|
-
return cachedInternals.renderSubagentResult(result, options, theme);
|
|
44
|
+
return cachedInternals.renderSubagentResult(result, options, theme, frame);
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
export type SubagentParamsLike = any;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const TAKOMI_SUBAGENT_HEARTBEAT_MS = 125;
|
|
2
|
+
|
|
3
|
+
type Timer = ReturnType<typeof setInterval>;
|
|
4
|
+
|
|
5
|
+
type HeartbeatState = {
|
|
6
|
+
takomiSubagentHeartbeatTimer?: Timer;
|
|
7
|
+
takomiSubagentHeartbeatFrame?: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type TakomiSubagentRenderContext = {
|
|
11
|
+
state: Record<string, unknown>;
|
|
12
|
+
invalidate?: () => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const activeHeartbeatStates = new Set<HeartbeatState>();
|
|
16
|
+
|
|
17
|
+
export function clearTakomiSubagentResultHeartbeat(context: TakomiSubagentRenderContext): void {
|
|
18
|
+
const state = context.state as HeartbeatState;
|
|
19
|
+
if (state.takomiSubagentHeartbeatTimer !== undefined) {
|
|
20
|
+
clearInterval(state.takomiSubagentHeartbeatTimer);
|
|
21
|
+
state.takomiSubagentHeartbeatTimer = undefined;
|
|
22
|
+
}
|
|
23
|
+
activeHeartbeatStates.delete(state);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function ensureTakomiSubagentResultHeartbeat(context: TakomiSubagentRenderContext): void {
|
|
27
|
+
const state = context.state as HeartbeatState;
|
|
28
|
+
if (state.takomiSubagentHeartbeatTimer !== undefined || typeof context.invalidate !== "function") return;
|
|
29
|
+
|
|
30
|
+
state.takomiSubagentHeartbeatFrame ??= 0;
|
|
31
|
+
state.takomiSubagentHeartbeatTimer = setInterval(() => {
|
|
32
|
+
state.takomiSubagentHeartbeatFrame = ((state.takomiSubagentHeartbeatFrame ?? 0) + 1) % 10;
|
|
33
|
+
try {
|
|
34
|
+
context.invalidate?.();
|
|
35
|
+
} catch {
|
|
36
|
+
// A replaced session makes its row context stale. Stop immediately rather
|
|
37
|
+
// than retaining the old UI through a failing interval callback.
|
|
38
|
+
clearTakomiSubagentResultHeartbeat(context);
|
|
39
|
+
}
|
|
40
|
+
}, TAKOMI_SUBAGENT_HEARTBEAT_MS);
|
|
41
|
+
state.takomiSubagentHeartbeatTimer.unref?.();
|
|
42
|
+
activeHeartbeatStates.add(state);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getTakomiSubagentHeartbeatFrame(context: TakomiSubagentRenderContext): number {
|
|
46
|
+
return (context.state as HeartbeatState).takomiSubagentHeartbeatFrame ?? 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function clearAllTakomiSubagentResultHeartbeats(): void {
|
|
50
|
+
for (const state of [...activeHeartbeatStates]) {
|
|
51
|
+
if (state.takomiSubagentHeartbeatTimer !== undefined) clearInterval(state.takomiSubagentHeartbeatTimer);
|
|
52
|
+
state.takomiSubagentHeartbeatTimer = undefined;
|
|
53
|
+
activeHeartbeatStates.delete(state);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
export type TakomiUxChecklistItem = {
|
|
4
|
+
id: string;
|
|
5
|
+
index: number;
|
|
6
|
+
text: string;
|
|
7
|
+
done: boolean;
|
|
8
|
+
stateSource: "input" | "agent-reported";
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type TakomiUxTask = {
|
|
12
|
+
agent: string;
|
|
13
|
+
task: string;
|
|
14
|
+
checklist: TakomiUxChecklistItem[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const CHECKBOX_LINE = /^\s*[-*+]\s+\[([ xX])\]\s+(.+?)\s*$/;
|
|
18
|
+
const ACCEPTANCE_BLOCK = /```acceptance-report\s*[\s\S]*?```/gi;
|
|
19
|
+
const OSC_SEQUENCE = /\x1b\][\s\S]*?(?:\x07|\x1b\\|$)/g;
|
|
20
|
+
const CSI_SEQUENCE = /(?:\x1b\[|\x9b)[0-?]*[ -\/]*[@-~]/g;
|
|
21
|
+
const ESC_SEQUENCE = /\x1b(?:[()][0-2A-Z0-9]|[=>]|[ -\/]*[@-~]?)/g;
|
|
22
|
+
const UNSAFE_CONTROLS = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/g;
|
|
23
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
24
|
+
|
|
25
|
+
/** Remove terminal control sequences before untrusted values reach Text/Markdown. */
|
|
26
|
+
export function sanitizeUntrustedText(value: string): string {
|
|
27
|
+
return value
|
|
28
|
+
.replace(OSC_SEQUENCE, "")
|
|
29
|
+
.replace(CSI_SEQUENCE, "")
|
|
30
|
+
.replace(ESC_SEQUENCE, "")
|
|
31
|
+
.replace(UNSAFE_CONTROLS, "");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Sanitize string leaves while retaining native result shape and Markdown syntax. */
|
|
35
|
+
export function sanitizeUntrustedValue<T>(value: T, seen = new WeakMap<object, unknown>()): T {
|
|
36
|
+
if (typeof value === "string") return sanitizeUntrustedText(value) as T;
|
|
37
|
+
if (!value || typeof value !== "object") return value;
|
|
38
|
+
if (seen.has(value as object)) return seen.get(value as object) as T;
|
|
39
|
+
const output: any = Array.isArray(value) ? [] : {};
|
|
40
|
+
seen.set(value as object, output);
|
|
41
|
+
for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
|
|
42
|
+
output[key] = sanitizeUntrustedValue(child, seen);
|
|
43
|
+
}
|
|
44
|
+
return output as T;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createTakomiUxTasks(
|
|
48
|
+
tasks: Array<{ agent: string; task: string; checklist?: Array<string | { text: string; done?: boolean }> }>,
|
|
49
|
+
): TakomiUxTask[] {
|
|
50
|
+
return tasks.map((task, taskIndex) => ({
|
|
51
|
+
agent: task.agent,
|
|
52
|
+
task: task.task,
|
|
53
|
+
checklist: (task.checklist ?? []).map((item, index) => ({
|
|
54
|
+
id: `task-${taskIndex + 1}-item-${index + 1}`,
|
|
55
|
+
index,
|
|
56
|
+
text: typeof item === "string" ? item : item.text,
|
|
57
|
+
done: typeof item === "string" ? false : item.done === true,
|
|
58
|
+
stateSource: "input",
|
|
59
|
+
})),
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function withTakomiUxDetails(details: unknown, tasks: TakomiUxTask[]): Record<string, unknown> {
|
|
64
|
+
const base = details && typeof details === "object" && !Array.isArray(details)
|
|
65
|
+
? details as Record<string, unknown>
|
|
66
|
+
: {};
|
|
67
|
+
return { ...base, takomiUx: { tasks } };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function textParts(content: unknown): string[] {
|
|
71
|
+
if (typeof content === "string") return content.trim() ? [content] : [];
|
|
72
|
+
if (!Array.isArray(content)) return [];
|
|
73
|
+
return content
|
|
74
|
+
.filter((part): part is { type: "text"; text: string } => (
|
|
75
|
+
Boolean(part) && typeof part === "object" && (part as any).type === "text" && typeof (part as any).text === "string"
|
|
76
|
+
))
|
|
77
|
+
.map((part) => part.text)
|
|
78
|
+
.filter((text) => text.trim().length > 0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Only explicit, visible assistant text is eligible for compact narrative. */
|
|
82
|
+
export function explicitAssistantTexts(result: any): string[] {
|
|
83
|
+
if (!Array.isArray(result?.messages)) return [];
|
|
84
|
+
return result.messages
|
|
85
|
+
.filter((message: any) => message?.role === "assistant")
|
|
86
|
+
.flatMap((message: any) => textParts(message.content));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Markdown boxes are not structured truth. Exact, unambiguous self-reports may
|
|
91
|
+
* update a uniquely-labelled input item and are retained as agent-reported.
|
|
92
|
+
*/
|
|
93
|
+
export function resolvedChecklist(items: TakomiUxChecklistItem[], assistantTexts: string[]): TakomiUxChecklistItem[] {
|
|
94
|
+
const states = items.map((item) => ({ ...item }));
|
|
95
|
+
const sourceCounts = new Map<string, number>();
|
|
96
|
+
for (const item of items) sourceCounts.set(item.text, (sourceCounts.get(item.text) ?? 0) + 1);
|
|
97
|
+
|
|
98
|
+
for (const text of assistantTexts) {
|
|
99
|
+
const reports = text.split(/\r?\n/).map((line) => line.match(CHECKBOX_LINE)).filter(Boolean) as RegExpMatchArray[];
|
|
100
|
+
const reportCounts = new Map<string, number>();
|
|
101
|
+
for (const report of reports) {
|
|
102
|
+
const label = report[2] ?? "";
|
|
103
|
+
reportCounts.set(label, (reportCounts.get(label) ?? 0) + 1);
|
|
104
|
+
}
|
|
105
|
+
for (const report of reports) {
|
|
106
|
+
const label = report[2] ?? "";
|
|
107
|
+
if (sourceCounts.get(label) !== 1 || reportCounts.get(label) !== 1) continue;
|
|
108
|
+
const item = states.find((candidate) => candidate.text === label);
|
|
109
|
+
if (!item) continue;
|
|
110
|
+
item.done = (report[1] ?? "").toLowerCase() === "x";
|
|
111
|
+
item.stateSource = "agent-reported";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return states;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function cleanNarrative(value: string): string {
|
|
118
|
+
return sanitizeUntrustedText(value)
|
|
119
|
+
.replace(ACCEPTANCE_BLOCK, "")
|
|
120
|
+
.replace(/\r/g, "")
|
|
121
|
+
.trim();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function meaningfulLines(value: string): string[] {
|
|
125
|
+
return cleanNarrative(value)
|
|
126
|
+
.split("\n")
|
|
127
|
+
.map((line) => line.replace(/\s+/g, " ").trim())
|
|
128
|
+
.filter(Boolean);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function clipVisibleLine(line: string, maxColumns: number): string {
|
|
132
|
+
const safe = sanitizeUntrustedText(line);
|
|
133
|
+
if (visibleWidth(safe) <= maxColumns) return safe;
|
|
134
|
+
const target = Math.max(0, maxColumns - 1);
|
|
135
|
+
let output = "";
|
|
136
|
+
let width = 0;
|
|
137
|
+
for (const { segment } of segmenter.segment(safe)) {
|
|
138
|
+
const nextWidth = visibleWidth(segment);
|
|
139
|
+
if (width + nextWidth > target) break;
|
|
140
|
+
output += segment;
|
|
141
|
+
width += nextWidth;
|
|
142
|
+
}
|
|
143
|
+
return `${output}…`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function boundNarrative(
|
|
147
|
+
value: string,
|
|
148
|
+
options: { maxLines: number; maxColumns: number; from?: "start" | "end" },
|
|
149
|
+
): { lines: string[]; truncated: boolean } {
|
|
150
|
+
const source = meaningfulLines(value);
|
|
151
|
+
const selected = options.from === "start" ? source.slice(0, options.maxLines) : source.slice(-options.maxLines);
|
|
152
|
+
const clipped = selected.map((line) => clipVisibleLine(line, options.maxColumns));
|
|
153
|
+
return {
|
|
154
|
+
lines: clipped,
|
|
155
|
+
truncated: source.length > selected.length || selected.some((line, index) => line !== clipped[index]),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function finalAnswer(result: any): string {
|
|
160
|
+
if (typeof result?.finalOutput === "string" && result.finalOutput.trim()) return cleanNarrative(result.finalOutput);
|
|
161
|
+
return cleanNarrative(explicitAssistantTexts(result).at(-1) ?? "");
|
|
162
|
+
}
|