@aeondave/pi-persona 1.17.1 → 1.17.4
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/AGENTS.md +103 -64
- package/CHANGELOG.md +310 -1
- package/README.md +37 -23
- package/agents/architect.md +21 -0
- package/agents/balthasar.md +17 -22
- package/agents/casper.md +17 -23
- package/agents/evidence-verifier.md +31 -16
- package/agents/melchior.md +17 -22
- package/agents/operator.md +15 -55
- package/agents/research.md +14 -48
- package/agents/reviewer.md +20 -39
- package/agents/scout.md +14 -15
- package/agents/verifier.md +15 -16
- package/docs/ARCHITECTURE.md +280 -92
- package/docs/COLLABORATION.md +110 -0
- package/docs/EXPERIENCE.md +52 -17
- package/docs/MONITORS.md +62 -3
- package/docs/README.md +12 -0
- package/docs/REFERENCE.md +139 -38
- package/docs/SPINE.md +2 -2
- package/docs/STRATEGIES.md +23 -8
- package/docs/TELEMETRY.md +1 -0
- package/flows/deep-review.flow.json +1 -1
- package/package.json +15 -8
- package/personas/audit.md +28 -17
- package/personas/dev.md +30 -32
- package/personas/elite.md +223 -221
- package/personas/judge.md +25 -19
- package/personas/magi.md +26 -29
- package/personas/planner.md +31 -24
- package/personas/researcher.md +25 -26
- package/personas/swarm.md +35 -25
- package/personas/verify.md +30 -19
- package/src/agents/agent.ts +26 -0
- package/src/bridge.ts +28 -10
- package/src/bus/broker/client.ts +119 -21
- package/src/bus/broker/host.ts +123 -8
- package/src/bus/broker/messages.ts +31 -4
- package/src/bus/contact.ts +113 -38
- package/src/bus/peers.ts +15 -0
- package/src/bus/result-reader.ts +88 -0
- package/src/core/brief.ts +436 -353
- package/src/core/capabilities.ts +1 -1
- package/src/core/config.ts +15 -10
- package/src/core/leg-thinking.ts +57 -0
- package/src/core/model-catalog.ts +171 -0
- package/src/core/model-tier.ts +89 -0
- package/src/core/models.ts +13 -4
- package/src/core/monitor-output.ts +85 -0
- package/src/core/monitor.ts +34 -2
- package/src/core/nudge.ts +176 -23
- package/src/core/ownership.ts +1 -2
- package/src/core/pi-compat.ts +5 -12
- package/src/core/seed.ts +99 -6
- package/src/core/session-runs.ts +87 -0
- package/src/core/settings.ts +1 -1
- package/src/core/timer.ts +34 -9
- package/src/core/types.ts +1 -1
- package/src/engine/adapter.ts +95 -27
- package/src/engine/async.ts +213 -155
- package/src/engine/child.ts +12 -4
- package/src/engine/host-module.ts +12 -0
- package/src/engine/inproc.ts +99 -53
- package/src/engine/result-grant.ts +62 -0
- package/src/engine/session-tools.ts +6 -0
- package/src/engine/stream.ts +10 -20
- package/src/engine/worktree.ts +17 -7
- package/src/exocom/collaboration-delivery.ts +29 -0
- package/src/exocom/collaboration.ts +148 -0
- package/src/exocom/envelope.ts +43 -4
- package/src/exocom/gate.ts +7 -2
- package/src/exocom/install.ts +47 -18
- package/src/exocom/ledger.ts +58 -8
- package/src/exocom/plane.ts +32 -1
- package/src/exocom/profile.ts +40 -0
- package/src/exocom/registry.ts +5 -0
- package/src/exocom/status.ts +11 -1
- package/src/exocom/write-guard.ts +7 -0
- package/src/extension/broker-host.ts +61 -9
- package/src/extension/engine.ts +54 -28
- package/src/extension/hooks.ts +952 -868
- package/src/extension/identity.ts +41 -75
- package/src/extension/persona-model-settings.ts +31 -13
- package/src/extension/results.ts +136 -0
- package/src/extension/shared.ts +32 -7
- package/src/extension.ts +2965 -2611
- package/src/loader.ts +11 -3
- package/src/monitor/archive.ts +235 -0
- package/src/monitor/session.ts +21 -1
- package/src/orchestration/flow-journal.ts +33 -6
- package/src/orchestration/flow-run.ts +160 -32
- package/src/orchestration/flow.ts +20 -2
- package/src/orchestration/reducers.ts +9 -5
- package/src/orchestration/render.ts +1 -1
- package/src/orchestration/roster.ts +71 -23
- package/src/orchestration/sdk.ts +61 -11
- package/src/orchestration/strategies/compete.ts +5 -1
- package/src/orchestration/strategies/council-rounds.ts +2 -0
- package/src/orchestration/strategies/critic-loop.ts +3 -0
- package/src/orchestration/strategies/debate.ts +2 -0
- package/src/orchestration/strategies/fanout.ts +3 -2
- package/src/orchestration/strategies/judge.ts +5 -1
- package/src/orchestration/strategies/magi.ts +40 -23
- package/src/orchestration/strategies/map.ts +13 -3
- package/src/orchestration/strategies/pair.ts +2 -0
- package/src/orchestration/strategies/pipeline.ts +2 -0
- package/src/orchestration/strategies/synthesize.ts +27 -7
- package/src/orchestration/strategy.ts +31 -0
- package/src/orchestration/types.ts +10 -0
- package/src/persona/model-participants.ts +20 -7
- package/src/persona/orchestrate.ts +2 -0
- package/src/persona/persona.ts +8 -4
- package/src/results/archive.ts +281 -0
- package/src/results/record.ts +166 -0
- package/src/results/retrieval.ts +142 -0
- package/src/results/types.ts +128 -0
- package/src/telemetry/contract.ts +1 -1
- package/src/tools/background-run.ts +151 -0
- package/src/tools/council.ts +108 -66
- package/src/tools/delegate-tool.ts +349 -283
- package/src/tools/delegate.ts +35 -216
- package/src/tools/exocom-collaboration-input.ts +168 -0
- package/src/tools/exocom-collaboration.ts +315 -0
- package/src/tools/exocom-profile.ts +102 -0
- package/src/tools/exocom-work.ts +18 -53
- package/src/tools/exocom.ts +35 -22
- package/src/tools/flow.ts +103 -30
- package/src/tools/intercom-tool.ts +202 -128
- package/src/tools/intercom.ts +1 -1
- package/src/tools/models.ts +34 -9
- package/src/tools/monitor.ts +58 -6
- package/src/tools/read-result.ts +52 -0
- package/src/tools/timer.ts +34 -15
- package/src/ui/agent-overlay.ts +65 -16
- package/src/ui/agent-tree.ts +127 -19
- package/src/ui/compact-widget.ts +25 -0
- package/src/ui/failure-summary.ts +55 -0
- package/src/ui/literal-tool-result.ts +24 -0
- package/src/ui/model-picker.ts +158 -81
- package/src/ui/orchestration-launch.ts +49 -0
- package/src/ui/persona-settings.ts +9 -3
- package/src/ui/presentation.ts +24 -0
- package/src/ui/report-body.ts +63 -0
- package/src/ui/session-cards.ts +366 -0
- package/src/ui/wake-rows.ts +262 -0
- package/teams.yaml +5 -1
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/** Task, offer, and evidence adapters for the external Exocom collaboration ledger. */
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { type Static } from "typebox";
|
|
4
|
+
|
|
5
|
+
import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
7
|
+
|
|
8
|
+
import { attributePeer } from "../core/fence.ts";
|
|
9
|
+
import { collaborationOfferStatus, collaborationStatus } from "../exocom/collaboration.ts";
|
|
10
|
+
import type { ExocomPlane } from "../exocom/plane.ts";
|
|
11
|
+
import type { ExocomSemanticFrame } from "../exocom/envelope.ts";
|
|
12
|
+
import type { LedgerState } from "../exocom/ledger.ts";
|
|
13
|
+
import { normalizeMetadataText } from "../exocom/registry.ts";
|
|
14
|
+
import { renderWriteSet, untrusted } from "../exocom/untrusted.ts";
|
|
15
|
+
import { renderLiteralToolResult } from "../ui/literal-tool-result.ts";
|
|
16
|
+
import { createReportBody } from "../ui/report-body.ts";
|
|
17
|
+
import {
|
|
18
|
+
OfferParams,
|
|
19
|
+
parseOfferInput,
|
|
20
|
+
parsePublishInput,
|
|
21
|
+
parseTaskInput,
|
|
22
|
+
PublishParams,
|
|
23
|
+
TaskParams,
|
|
24
|
+
} from "./exocom-collaboration-input.ts";
|
|
25
|
+
|
|
26
|
+
export interface ExocomCollaborationDeps {
|
|
27
|
+
getPlane: () => ExocomPlane | undefined;
|
|
28
|
+
sessionId: () => string;
|
|
29
|
+
name: () => string;
|
|
30
|
+
now: () => number;
|
|
31
|
+
/** False for joined foreign workspaces whose file paths differ from the shared ledger scope. */
|
|
32
|
+
canClaim: () => boolean;
|
|
33
|
+
/** Recheck the effective capability at execution time, including targeted tool denies. */
|
|
34
|
+
canCallTool: (name: string) => boolean;
|
|
35
|
+
/** Canonicalize a public exocom_list target to its authenticated raw session id. */
|
|
36
|
+
resolveTarget: (target: string) => string;
|
|
37
|
+
dispatch: (frame: ExocomSemanticFrame) => Promise<{ msg_id: string; peerWakeDeferred?: true }>;
|
|
38
|
+
ledger: () => LedgerState;
|
|
39
|
+
/** Resolve attribution from the live registry; self-reported frame names are never used. */
|
|
40
|
+
labelFor: (sessionId: string) => string | undefined;
|
|
41
|
+
/** Exact public routing target shown by exocom_list for a live session. */
|
|
42
|
+
targetForSession: (sessionId: string) => string | undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function toolResult(text: string, details: Record<string, unknown>) {
|
|
46
|
+
return { content: [{ type: "text" as const, text }], details };
|
|
47
|
+
}
|
|
48
|
+
function checkedIdentity(deps: ExocomCollaborationDeps) {
|
|
49
|
+
if (!deps.getPlane()) throw new Error("exocom is not active for this persona");
|
|
50
|
+
const now = deps.now();
|
|
51
|
+
if (!Number.isFinite(now) || !Number.isFinite(new Date(now).getTime())) throw new Error("exocom: clock returned an invalid timestamp");
|
|
52
|
+
return { from_session: deps.sessionId(), from_name: deps.name(), ts: new Date(now).toISOString(), now };
|
|
53
|
+
}
|
|
54
|
+
function requireToolPermission(deps: ExocomCollaborationDeps, tool: string): void {
|
|
55
|
+
if (deps.canCallTool(tool) !== true) throw new Error(`${tool}: this persona cannot call this Exocom tool`);
|
|
56
|
+
}
|
|
57
|
+
function frameIdentity(identity: ReturnType<typeof checkedIdentity>) {
|
|
58
|
+
return { from_session: identity.from_session, from_name: identity.from_name, ts: identity.ts };
|
|
59
|
+
}
|
|
60
|
+
function requireWriteClaim(deps: ExocomCollaborationDeps, tool: string): void {
|
|
61
|
+
if (deps.canClaim() !== true || deps.canCallTool("exocom_claim") !== true) {
|
|
62
|
+
throw new Error(`${tool}: write-set tasks require local workspace scope and callable exocom_claim permission`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function taskByKey(state: LedgerState, workKey: string) {
|
|
66
|
+
return state.collaboration?.tasks.find((task) => task.created.work_key === workKey);
|
|
67
|
+
}
|
|
68
|
+
function taskByIdentity(state: LedgerState, workKey: string, taskId: string) {
|
|
69
|
+
return state.collaboration?.tasks.find((task) => task.created.work_key === workKey && task.created.msg_id === taskId);
|
|
70
|
+
}
|
|
71
|
+
function dispatchNotice(deferred: boolean): string {
|
|
72
|
+
return deferred ? " · ledger committed; peer wake deferred" : "";
|
|
73
|
+
}
|
|
74
|
+
function dispatchDetails(deferred: boolean): Record<string, unknown> {
|
|
75
|
+
return deferred ? { peerWakeDeferred: true } : {};
|
|
76
|
+
}
|
|
77
|
+
function resolvePeerLabel(deps: ExocomCollaborationDeps, sessionId: string): string {
|
|
78
|
+
if (sessionId === deps.sessionId()) return `you (${untrusted(deps.name(), 48) || "self"})`;
|
|
79
|
+
return deps.labelFor(sessionId) ?? sessionId;
|
|
80
|
+
}
|
|
81
|
+
function listPage(deps: ExocomCollaborationDeps, offset: number, limit: number, detail: boolean) {
|
|
82
|
+
if (detail && limit > 5) throw new Error("exocom_task: limit must be at most 5 when detail=true");
|
|
83
|
+
const state = deps.ledger();
|
|
84
|
+
const tasks = state.collaboration?.tasks ?? [];
|
|
85
|
+
const offers = state.collaboration?.offers ?? [];
|
|
86
|
+
const total = tasks.length;
|
|
87
|
+
const page = tasks.slice(offset, offset + limit);
|
|
88
|
+
const lines = [`exocom tasks · ${total} retained · showing ${page.length} from offset ${offset}`];
|
|
89
|
+
for (const task of page) {
|
|
90
|
+
const created = task.created;
|
|
91
|
+
const status = collaborationStatus(task);
|
|
92
|
+
const creatorLabel = untrusted(resolvePeerLabel(deps, created.from_session), 64) || created.from_session;
|
|
93
|
+
lines.push(`\n${created.work_key} · task_id=${created.msg_id} · ${status} · created by ${creatorLabel} · ${created.write_set.length ? `${created.write_set.length} write paths` : "read-only"}`);
|
|
94
|
+
if (task.taken) {
|
|
95
|
+
const takerLabel = untrusted(resolvePeerLabel(deps, task.taken.from_session), 64) || task.taken.from_session;
|
|
96
|
+
lines.push(`taker: ${takerLabel}`);
|
|
97
|
+
}
|
|
98
|
+
if (created.write_set.length) {
|
|
99
|
+
lines.push(`write scope summary (may be clipped; use exocom_task get with work_key=${created.work_key} and task_id=${created.msg_id} for exact paths)`);
|
|
100
|
+
lines.push(attributePeer(resolvePeerLabel(deps, created.from_session), renderWriteSet(created.write_set)));
|
|
101
|
+
}
|
|
102
|
+
const taskOffers = offers.filter((offer) => offer.work_key === created.work_key && offer.task_id === created.msg_id);
|
|
103
|
+
for (const offer of taskOffers.slice(0, 3)) {
|
|
104
|
+
const target = offer.to_session
|
|
105
|
+
? normalizeMetadataText(deps.targetForSession(offer.to_session) ?? `offline:${offer.to_session}`, 80, "offline target")
|
|
106
|
+
: "pool";
|
|
107
|
+
const offerState = collaborationOfferStatus(offer, tasks, deps.now());
|
|
108
|
+
lines.push(`offer ${offer.offer_id} → ${target} · ${offerState} · expires ${offer.expires_at}`);
|
|
109
|
+
}
|
|
110
|
+
if (taskOffers.length > 3) lines.push(`offers: +${taskOffers.length - 3} more`);
|
|
111
|
+
if (detail) {
|
|
112
|
+
lines.push(attributePeer(resolvePeerLabel(deps, created.from_session), created.description));
|
|
113
|
+
if (task.progress) lines.push(attributePeer(resolvePeerLabel(deps, task.progress.from_session), `Progress: ${task.progress.note}`));
|
|
114
|
+
if (task.finished) lines.push(attributePeer(resolvePeerLabel(deps, task.finished.from_session), `Finish (${task.finished.outcome}): ${task.finished.note}`));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const nextOffset = offset + page.length < total ? offset + page.length : undefined;
|
|
118
|
+
return toolResult(lines.join("\n"), {
|
|
119
|
+
action: "list", total, offset, limit, count: page.length,
|
|
120
|
+
task_ids: page.map((task) => task.created.msg_id),
|
|
121
|
+
...(nextOffset === undefined ? {} : { nextOffset }), detail,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function taskDetail(deps: ExocomCollaborationDeps, state: LedgerState, task: NonNullable<ReturnType<typeof taskByIdentity>>) {
|
|
126
|
+
const created = task.created;
|
|
127
|
+
const status = collaborationStatus(task);
|
|
128
|
+
const creator = resolvePeerLabel(deps, created.from_session);
|
|
129
|
+
const lines = [
|
|
130
|
+
`exocom task · ${created.work_key} · task_id=${created.msg_id} · ${status}`,
|
|
131
|
+
`created by ${untrusted(creator, 64) || created.from_session}`,
|
|
132
|
+
attributePeer(creator, `Description:\n${created.description}`),
|
|
133
|
+
];
|
|
134
|
+
if (created.write_set.length) {
|
|
135
|
+
lines.push(attributePeer(creator, `Exact write_set JSON (${created.write_set.length} paths):\n${JSON.stringify(created.write_set, null, 2)}`));
|
|
136
|
+
} else {
|
|
137
|
+
lines.push("write_set: [] (read-only)");
|
|
138
|
+
}
|
|
139
|
+
if (task.taken) lines.push(`taken by ${untrusted(resolvePeerLabel(deps, task.taken.from_session), 64) || task.taken.from_session}`);
|
|
140
|
+
if (task.progress) lines.push(attributePeer(resolvePeerLabel(deps, task.progress.from_session), `Progress:\n${task.progress.note}`));
|
|
141
|
+
if (task.finished) lines.push(attributePeer(resolvePeerLabel(deps, task.finished.from_session), `Finish (${task.finished.outcome}):\n${task.finished.note}`));
|
|
142
|
+
const offers = (state.collaboration?.offers ?? []).filter((offer) => offer.work_key === created.work_key && offer.task_id === created.msg_id);
|
|
143
|
+
for (const offer of offers.slice(0, 3)) {
|
|
144
|
+
const target = offer.to_session
|
|
145
|
+
? normalizeMetadataText(deps.targetForSession(offer.to_session) ?? `offline:${offer.to_session}`, 80, "offline target")
|
|
146
|
+
: "pool";
|
|
147
|
+
lines.push(`offer ${offer.offer_id} → ${target} · ${collaborationOfferStatus(offer, state.collaboration?.tasks ?? [], deps.now())} · expires ${offer.expires_at}`);
|
|
148
|
+
}
|
|
149
|
+
if (offers.length > 3) lines.push(`offers: +${offers.length - 3} more`);
|
|
150
|
+
return toolResult(lines.join("\n\n"), {
|
|
151
|
+
action: "get", work_key: created.work_key, task_id: created.msg_id, status,
|
|
152
|
+
writeSetCount: created.write_set.length, offerIds: offers.map((offer) => offer.offer_id),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function registerExocomCollaborationTools(pi: ExtensionAPI, deps: ExocomCollaborationDeps): void {
|
|
157
|
+
pi.registerTool({
|
|
158
|
+
name: "exocom_task",
|
|
159
|
+
label: "Exocom Task",
|
|
160
|
+
description: "Create or pick up a bounded collaboration task, report progress or finish, inspect full details with get by work_key+task_id, and page through retained tasks. For discovery, use list without work_key/task_id. List pages default to 10 rows (5 with detail=true), cap at 20 summaries and 5 detailed rows; explicit detail limits above 5 are rejected. List write scopes are summaries that may be clipped; get requires both identifiers returned by create/list and shows the exact fenced scope. Write-set tasks require workspace-scoped exocom_claim permission; a foreign-workspace Pi may take read-only tasks.",
|
|
161
|
+
parameters: TaskParams,
|
|
162
|
+
async execute(_id, raw: Static<typeof TaskParams>) {
|
|
163
|
+
const params = parseTaskInput(raw);
|
|
164
|
+
requireToolPermission(deps, "exocom_task");
|
|
165
|
+
if (params.action === "list") {
|
|
166
|
+
checkedIdentity(deps);
|
|
167
|
+
return listPage(deps, params.offset, params.limit, params.detail);
|
|
168
|
+
}
|
|
169
|
+
const identity = checkedIdentity(deps);
|
|
170
|
+
if (params.action === "create") {
|
|
171
|
+
if (params.write_set.length) requireWriteClaim(deps, "exocom_task create");
|
|
172
|
+
if (taskByKey(deps.ledger(), params.work_key)) throw new Error(`exocom_task: work_key ${params.work_key} already exists`);
|
|
173
|
+
const frame: ExocomSemanticFrame = {
|
|
174
|
+
kind: "task_create", ...frameIdentity(identity), work_key: params.work_key,
|
|
175
|
+
description: params.description, write_set: params.write_set, msg_id: randomUUID(),
|
|
176
|
+
};
|
|
177
|
+
const sent = await deps.dispatch(frame);
|
|
178
|
+
return toolResult(`exocom: created task ${params.work_key} · task_id=${sent.msg_id}${dispatchNotice(sent.peerWakeDeferred === true)}`, {
|
|
179
|
+
action: "create", work_key: params.work_key, task_id: sent.msg_id, msg_id: sent.msg_id, writeSetCount: params.write_set.length,
|
|
180
|
+
...dispatchDetails(sent.peerWakeDeferred === true),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
const state = deps.ledger();
|
|
184
|
+
const task = taskByIdentity(state, params.work_key, params.task_id);
|
|
185
|
+
if (!task) throw new Error(`exocom_task: task_id ${params.task_id} does not match retained task ${params.work_key}`);
|
|
186
|
+
if (params.action === "get") return taskDetail(deps, state, task);
|
|
187
|
+
if (params.action === "take") {
|
|
188
|
+
if (task.created.write_set.length) requireWriteClaim(deps, "exocom_task take");
|
|
189
|
+
if (collaborationStatus(task) !== "open") throw new Error(`exocom_task: task is ${collaborationStatus(task)}; pickup refused`);
|
|
190
|
+
const frame: ExocomSemanticFrame = { kind: "task_take", ...frameIdentity(identity), work_key: params.work_key, task_id: params.task_id, msg_id: randomUUID() };
|
|
191
|
+
const sent = await deps.dispatch(frame);
|
|
192
|
+
return toolResult(`exocom: took task ${params.work_key} · task_id=${params.task_id} · msg_id=${sent.msg_id}${dispatchNotice(sent.peerWakeDeferred === true)}`, {
|
|
193
|
+
action: "take", work_key: params.work_key, task_id: params.task_id, msg_id: sent.msg_id, ...dispatchDetails(sent.peerWakeDeferred === true),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
if (task.taken?.from_session !== identity.from_session || collaborationStatus(task) !== "taken") {
|
|
197
|
+
throw new Error(`exocom_task ${params.action}: only the active task taker may report progress or finish`);
|
|
198
|
+
}
|
|
199
|
+
let frame: ExocomSemanticFrame;
|
|
200
|
+
if (params.action === "progress") {
|
|
201
|
+
frame = { kind: "progress", ...frameIdentity(identity), work_key: params.work_key, task_id: params.task_id, note: params.note, msg_id: randomUUID() };
|
|
202
|
+
} else {
|
|
203
|
+
frame = { kind: "task_finish", ...frameIdentity(identity), work_key: params.work_key, task_id: params.task_id, outcome: params.outcome, note: params.note, msg_id: randomUUID() };
|
|
204
|
+
}
|
|
205
|
+
const sent = await deps.dispatch(frame);
|
|
206
|
+
return toolResult(`exocom: ${params.action} ${params.work_key} · task_id=${params.task_id} · msg_id=${sent.msg_id}`, {
|
|
207
|
+
action: params.action, work_key: params.work_key, task_id: params.task_id, msg_id: sent.msg_id,
|
|
208
|
+
...(params.action === "finish" ? { outcome: params.outcome } : {}),
|
|
209
|
+
...dispatchDetails(sent.peerWakeDeferred === true),
|
|
210
|
+
});
|
|
211
|
+
},
|
|
212
|
+
renderCall(args, theme) {
|
|
213
|
+
return new Text(`${theme.fg("toolTitle", theme.bold("Exocom Task "))}${theme.fg("accent", normalizeMetadataText(args.action, 24, "task"))}${args.work_key ? ` ${normalizeMetadataText(args.work_key, 80, "work")}` : ""}`, 0, 0);
|
|
214
|
+
},
|
|
215
|
+
renderResult(res, options, theme, context) {
|
|
216
|
+
if (options.expanded && !(context?.isError ?? res.isError) && (res.details?.detail === true || res.details?.action === "get")) {
|
|
217
|
+
const text = res.content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
|
|
218
|
+
return createReportBody(text, theme, {}, context?.lastComponent);
|
|
219
|
+
}
|
|
220
|
+
return renderLiteralToolResult(res, options, theme, context);
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
pi.registerTool({
|
|
225
|
+
name: "exocom_offer",
|
|
226
|
+
label: "Exocom Offer",
|
|
227
|
+
description: "Offer an open task to one peer from exocom_list or to the pool. Expiration defaults to one hour and is capped at 24 hours.",
|
|
228
|
+
parameters: OfferParams,
|
|
229
|
+
async execute(_id, raw: Static<typeof OfferParams>) {
|
|
230
|
+
const params = parseOfferInput(raw);
|
|
231
|
+
requireToolPermission(deps, "exocom_offer");
|
|
232
|
+
const identity = checkedIdentity(deps);
|
|
233
|
+
const task = taskByIdentity(deps.ledger(), params.work_key, params.task_id);
|
|
234
|
+
if (!task) throw new Error(`exocom_offer: task_id ${params.task_id} does not match retained task ${params.work_key}`);
|
|
235
|
+
if (collaborationStatus(task) !== "open") throw new Error(`exocom_offer: offers require an open task, current status is ${collaborationStatus(task)}`);
|
|
236
|
+
let to_session: string | undefined;
|
|
237
|
+
if (params.target !== undefined) {
|
|
238
|
+
to_session = deps.resolveTarget(params.target);
|
|
239
|
+
if (to_session === identity.from_session) throw new Error("exocom_offer: target cannot be this session");
|
|
240
|
+
}
|
|
241
|
+
const offerId = randomUUID();
|
|
242
|
+
const expires_at = new Date(identity.now + params.expiresInMinutes * 60_000).toISOString();
|
|
243
|
+
const frame: ExocomSemanticFrame = {
|
|
244
|
+
kind: "offer", ...frameIdentity(identity), work_key: params.work_key, task_id: params.task_id, offer_id: offerId, expires_at,
|
|
245
|
+
...(to_session === undefined ? {} : { to_session }), msg_id: randomUUID(),
|
|
246
|
+
};
|
|
247
|
+
const sent = await deps.dispatch(frame);
|
|
248
|
+
const targetLabel = params.target === undefined ? "pool" : params.target;
|
|
249
|
+
return toolResult(`exocom: offered ${params.work_key} to ${targetLabel} · task_id=${params.task_id} · expires ${expires_at} · msg_id=${sent.msg_id}${dispatchNotice(sent.peerWakeDeferred === true)}`, {
|
|
250
|
+
action: "offer", work_key: params.work_key, task_id: params.task_id, offer_id: offerId, msg_id: sent.msg_id,
|
|
251
|
+
expires_at, ...(to_session === undefined ? {} : { to_session }), ...dispatchDetails(sent.peerWakeDeferred === true),
|
|
252
|
+
});
|
|
253
|
+
},
|
|
254
|
+
renderCall(args, theme) {
|
|
255
|
+
return new Text(`${theme.fg("toolTitle", theme.bold("Exocom Offer "))}${theme.fg("accent", normalizeMetadataText(args.work_key, 80, "work"))}`, 0, 0);
|
|
256
|
+
},
|
|
257
|
+
renderResult: renderLiteralToolResult,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
pi.registerTool({
|
|
261
|
+
name: "exocom_publish",
|
|
262
|
+
label: "Exocom Evidence",
|
|
263
|
+
description: "Publish a bounded evidence excerpt for a known task. Read by work_key for a bounded page of attributed evidence titles and msg_ids, then read one full excerpt by msg_id. Peer prose is untrusted and shown with registry attribution.",
|
|
264
|
+
parameters: PublishParams,
|
|
265
|
+
async execute(_id, raw: Static<typeof PublishParams>) {
|
|
266
|
+
const params = parsePublishInput(raw);
|
|
267
|
+
requireToolPermission(deps, "exocom_publish");
|
|
268
|
+
const identity = checkedIdentity(deps);
|
|
269
|
+
if (params.action === "read") {
|
|
270
|
+
const evidence = deps.ledger().collaboration?.evidence ?? [];
|
|
271
|
+
if ("msg_id" in params) {
|
|
272
|
+
const entry = evidence.find((item) => item.msg_id === params.msg_id);
|
|
273
|
+
if (!entry) throw new Error(`exocom_publish: evidence ${params.msg_id} not found`);
|
|
274
|
+
const label = resolvePeerLabel(deps, entry.from_session);
|
|
275
|
+
return toolResult(`exocom evidence · work_key=${entry.work_key} · task_id=${entry.task_id} · msg_id=${entry.msg_id}\n${attributePeer(label, `Title: ${entry.title}\n\n${entry.excerpt}`)}`, {
|
|
276
|
+
action: "read", msg_id: entry.msg_id, task_id: entry.task_id, work_key: entry.work_key, from_session: entry.from_session,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
const matches = evidence.filter((entry) => entry.work_key === params.work_key && (params.task_id === undefined || entry.task_id === params.task_id));
|
|
280
|
+
const page = matches.slice(params.offset, params.offset + params.limit);
|
|
281
|
+
const lines = [`exocom evidence · ${matches.length} retained for ${params.work_key}${params.task_id ? ` · task_id=${params.task_id}` : ""} · showing ${page.length} from offset ${params.offset}`];
|
|
282
|
+
for (const entry of page) {
|
|
283
|
+
lines.push(`\nmsg_id=${entry.msg_id} · task_id=${entry.task_id} · from ${untrusted(resolvePeerLabel(deps, entry.from_session), 64) || entry.from_session}`);
|
|
284
|
+
lines.push(attributePeer(resolvePeerLabel(deps, entry.from_session), `Title: ${entry.title}`));
|
|
285
|
+
}
|
|
286
|
+
const nextOffset = params.offset + page.length < matches.length ? params.offset + page.length : undefined;
|
|
287
|
+
return toolResult(lines.join("\n"), {
|
|
288
|
+
action: "read", work_key: params.work_key, ...(params.task_id ? { task_id: params.task_id } : {}), offset: params.offset, limit: params.limit,
|
|
289
|
+
count: page.length, total: matches.length, ...(nextOffset === undefined ? {} : { nextOffset }),
|
|
290
|
+
msg_ids: page.map((entry) => entry.msg_id), task_ids: [...new Set(page.map((entry) => entry.task_id))],
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
if (!taskByIdentity(deps.ledger(), params.work_key, params.task_id)) throw new Error(`exocom_publish: task_id ${params.task_id} does not match retained task ${params.work_key}`);
|
|
294
|
+
const frame: ExocomSemanticFrame = {
|
|
295
|
+
kind: "publish", ...frameIdentity(identity), work_key: params.work_key, task_id: params.task_id, title: params.title,
|
|
296
|
+
excerpt: params.excerpt, msg_id: randomUUID(),
|
|
297
|
+
};
|
|
298
|
+
const sent = await deps.dispatch(frame);
|
|
299
|
+
return toolResult(`exocom: published evidence for ${params.work_key} · task_id=${params.task_id} · msg_id=${sent.msg_id}${dispatchNotice(sent.peerWakeDeferred === true)}`, {
|
|
300
|
+
action: "publish", work_key: params.work_key, task_id: params.task_id, msg_id: sent.msg_id, ...dispatchDetails(sent.peerWakeDeferred === true),
|
|
301
|
+
});
|
|
302
|
+
},
|
|
303
|
+
renderCall(args, theme) {
|
|
304
|
+
const label = args.action === "read" ? normalizeMetadataText(args.msg_id ?? args.work_key, 80, "evidence") : normalizeMetadataText(args.work_key, 80, "work");
|
|
305
|
+
return new Text(`${theme.fg("toolTitle", theme.bold("Exocom Evidence "))}${theme.fg("accent", label)}`, 0, 0);
|
|
306
|
+
},
|
|
307
|
+
renderResult(res, options, theme, context) {
|
|
308
|
+
if (options.expanded && !(context?.isError ?? res.isError) && res.details?.action === "read") {
|
|
309
|
+
const text = res.content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
|
|
310
|
+
return createReportBody(text, theme, {}, context?.lastComponent);
|
|
311
|
+
}
|
|
312
|
+
return renderLiteralToolResult(res, options, theme, context);
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** Self-reported Exocom presence. This metadata never grants authority or changes tool gates. */
|
|
2
|
+
import { type Static, Type } from "typebox";
|
|
3
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
5
|
+
import { normalizeExocomProfile, type ExocomProfile } from "../exocom/profile.ts";
|
|
6
|
+
import type { ExocomPlane } from "../exocom/plane.ts";
|
|
7
|
+
import { renderLiteralToolResult } from "../ui/literal-tool-result.ts";
|
|
8
|
+
|
|
9
|
+
const ExocomProfileParams = Type.Object({
|
|
10
|
+
action: Type.Union([Type.Literal("get"), Type.Literal("set"), Type.Literal("clear")], {
|
|
11
|
+
description: "get the current self-report · set selected fields · clear it back to unknown",
|
|
12
|
+
}),
|
|
13
|
+
capabilities: Type.Optional(Type.Array(Type.String({ minLength: 1, maxLength: 48 }), {
|
|
14
|
+
maxItems: 8,
|
|
15
|
+
description: "set: up to 8 self-reported capability labels, each at most 48 characters",
|
|
16
|
+
})),
|
|
17
|
+
availability: Type.Optional(Type.Union([
|
|
18
|
+
Type.Literal("available"), Type.Literal("busy"), Type.Literal("away"),
|
|
19
|
+
], { description: "set: your self-reported current availability; omitted remains unknown" })),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
interface ProfileToolDetails {
|
|
23
|
+
action: "get" | "set" | "clear";
|
|
24
|
+
profile?: ExocomProfile;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function formatProfile(profile: ExocomProfile | undefined): string {
|
|
28
|
+
if (!profile) return "No profile reported; availability is unknown.";
|
|
29
|
+
const availability = profile.availability ?? "unknown";
|
|
30
|
+
const capabilities = profile.capabilities.length > 0 ? profile.capabilities.join(" · ") : "not reported";
|
|
31
|
+
return `Self-reported availability: ${availability}\nSelf-reported capabilities: ${capabilities}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function actionText(details: ProfileToolDetails): string {
|
|
35
|
+
if (details.action === "clear") return "Profile cleared; availability is unknown.";
|
|
36
|
+
return formatProfile(details.profile);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** `getPlane` is read on each call so a revoked Exocom plane cannot leave a stale mutator alive. */
|
|
40
|
+
export function registerExocomProfileTool(pi: ExtensionAPI, getPlane: () => ExocomPlane | undefined): void {
|
|
41
|
+
pi.registerTool({
|
|
42
|
+
name: "exocom_profile",
|
|
43
|
+
label: "Exocom Profile",
|
|
44
|
+
description: [
|
|
45
|
+
"Report your own optional Exocom presence. `get` reads it, `set` updates the supplied fields,",
|
|
46
|
+
"and `clear` removes it. Capabilities and availability are self-reported metadata only; they",
|
|
47
|
+
"do not prove identity, grant permissions, reserve work, or change tool access. An unset",
|
|
48
|
+
"availability is unknown; a partial update leaves an existing value unchanged. The profile lasts only for this Exocom session.",
|
|
49
|
+
].join(" "),
|
|
50
|
+
parameters: ExocomProfileParams,
|
|
51
|
+
async execute(_toolCallId, params: Static<typeof ExocomProfileParams>): Promise<{
|
|
52
|
+
content: [{ type: "text"; text: string }];
|
|
53
|
+
details: ProfileToolDetails;
|
|
54
|
+
}> {
|
|
55
|
+
const action = params && typeof params === "object" ? (params as { action?: unknown }).action : undefined;
|
|
56
|
+
if (typeof action !== "string" || !["get", "set", "clear"].includes(action)) {
|
|
57
|
+
throw new Error("exocom_profile: action must be get, set, or clear");
|
|
58
|
+
}
|
|
59
|
+
const plane = getPlane();
|
|
60
|
+
if (!plane) throw new Error("exocom is not active for this persona");
|
|
61
|
+
if (params.action === "get") {
|
|
62
|
+
const profile = normalizeExocomProfile(plane.getProfile());
|
|
63
|
+
return { content: [{ type: "text" as const, text: formatProfile(profile) }], details: { action: "get", ...(profile ? { profile } : {}) } satisfies ProfileToolDetails };
|
|
64
|
+
}
|
|
65
|
+
if (params.action === "clear") {
|
|
66
|
+
plane.setProfile(undefined);
|
|
67
|
+
return { content: [{ type: "text" as const, text: "Profile cleared; availability is unknown." }], details: { action: "clear" } satisfies ProfileToolDetails };
|
|
68
|
+
}
|
|
69
|
+
if (params.capabilities === undefined && params.availability === undefined) {
|
|
70
|
+
throw new Error("exocom_profile set needs at least one of { capabilities, availability }");
|
|
71
|
+
}
|
|
72
|
+
const current = normalizeExocomProfile(plane.getProfile());
|
|
73
|
+
const next = normalizeExocomProfile({
|
|
74
|
+
capabilities: params.capabilities ?? current?.capabilities ?? [],
|
|
75
|
+
...(params.availability ?? current?.availability
|
|
76
|
+
? { availability: params.availability ?? current?.availability }
|
|
77
|
+
: {}),
|
|
78
|
+
});
|
|
79
|
+
if (!next) throw new Error("exocom_profile set needs at least one visible capability or an availability value");
|
|
80
|
+
plane.setProfile(next);
|
|
81
|
+
return {
|
|
82
|
+
content: [{ type: "text" as const, text: formatProfile(next) }],
|
|
83
|
+
details: { action: "set", profile: next } satisfies ProfileToolDetails,
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
renderCall(args, theme) {
|
|
87
|
+
const action = typeof args.action === "string" ? args.action : "get";
|
|
88
|
+
return new Text(theme.fg("toolTitle", theme.bold(`Exocom Profile ${action}`)), 0, 0);
|
|
89
|
+
},
|
|
90
|
+
renderResult(result, { expanded }, theme, context) {
|
|
91
|
+
if (context?.isError || result.isError) return renderLiteralToolResult(result, { expanded }, theme, context);
|
|
92
|
+
const details = result.details as unknown as ProfileToolDetails | undefined;
|
|
93
|
+
if (!details || !["get", "set", "clear"].includes(details.action)) {
|
|
94
|
+
return renderLiteralToolResult(result, { expanded }, theme, { isError: true });
|
|
95
|
+
}
|
|
96
|
+
return renderLiteralToolResult({
|
|
97
|
+
...result,
|
|
98
|
+
content: [{ type: "text", text: actionText(details) }],
|
|
99
|
+
}, { expanded }, theme);
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
package/src/tools/exocom-work.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { type Static, Type } from "typebox";
|
|
7
7
|
|
|
8
|
-
import { type ExtensionAPI
|
|
8
|
+
import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { Text } from "@earendil-works/pi-tui";
|
|
10
10
|
|
|
11
11
|
import { attributePeer } from "../core/fence.ts";
|
|
@@ -15,6 +15,8 @@ import type { ExocomPlane } from "../exocom/plane.ts";
|
|
|
15
15
|
import type { ExocomSemanticFrame } from "../exocom/envelope.ts";
|
|
16
16
|
import { EXOCOM_WAIT_MAX_MS, waitTimeoutMs } from "../exocom/wait.ts";
|
|
17
17
|
import { normalizeMetadataText } from "../exocom/registry.ts";
|
|
18
|
+
import { renderLiteralToolResult } from "../ui/literal-tool-result.ts";
|
|
19
|
+
import { createReportBody } from "../ui/report-body.ts";
|
|
18
20
|
|
|
19
21
|
const TOKEN = { minLength: 1, maxLength: 128, pattern: "^[A-Za-z0-9._:-]+$" } as const;
|
|
20
22
|
|
|
@@ -90,11 +92,6 @@ function result(text: string, details: Record<string, unknown>) {
|
|
|
90
92
|
return { content: [{ type: "text" as const, text }], details };
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
function workToolFailed(renderedResult: unknown): boolean {
|
|
94
|
-
return !!renderedResult && typeof renderedResult === "object"
|
|
95
|
-
&& (renderedResult as { isError?: unknown }).isError === true;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
95
|
export function registerExocomWorkTools(pi: ExtensionAPI, deps: ExocomWorkDeps): void {
|
|
99
96
|
const identity = () => {
|
|
100
97
|
const plane = deps.getPlane();
|
|
@@ -122,12 +119,7 @@ export function registerExocomWorkTools(pi: ExtensionAPI, deps: ExocomWorkDeps):
|
|
|
122
119
|
renderCall(args, theme) {
|
|
123
120
|
return new Text(`${theme.fg("toolTitle", theme.bold("Exocom Claim "))}${theme.fg("accent", normalizeMetadataText(args.work_key, 80, "work"))}`, 0, 0);
|
|
124
121
|
},
|
|
125
|
-
renderResult
|
|
126
|
-
const first = res.content.find((item) => item.type === "text");
|
|
127
|
-
const text = first?.type === "text" ? first.text : "Exocom claim failed";
|
|
128
|
-
if (workToolFailed(res)) return new Text(theme.fg("error", text), 0, 0);
|
|
129
|
-
return new Text(theme.fg(expanded ? "toolOutput" : "accent", expanded ? text : `${text} (${keyHint("app.tools.expand", "to expand")})`), 0, 0);
|
|
130
|
-
},
|
|
122
|
+
renderResult: renderLiteralToolResult,
|
|
131
123
|
});
|
|
132
124
|
|
|
133
125
|
pi.registerTool({
|
|
@@ -161,12 +153,7 @@ export function registerExocomWorkTools(pi: ExtensionAPI, deps: ExocomWorkDeps):
|
|
|
161
153
|
renderCall(args, theme) {
|
|
162
154
|
return new Text(`${theme.fg("toolTitle", theme.bold("Exocom Ask "))}${theme.fg("accent", normalizeMetadataText(args.target, 80, "peer"))}`, 0, 0);
|
|
163
155
|
},
|
|
164
|
-
renderResult
|
|
165
|
-
const first = res.content.find((item) => item.type === "text");
|
|
166
|
-
const text = first?.type === "text" ? first.text : "Exocom ask failed";
|
|
167
|
-
if (workToolFailed(res)) return new Text(theme.fg("error", text), 0, 0);
|
|
168
|
-
return new Text(theme.fg(expanded ? "toolOutput" : "accent", expanded ? text : `${text} (${keyHint("app.tools.expand", "to expand")})`), 0, 0);
|
|
169
|
-
},
|
|
156
|
+
renderResult: renderLiteralToolResult,
|
|
170
157
|
});
|
|
171
158
|
|
|
172
159
|
pi.registerTool({
|
|
@@ -191,12 +178,7 @@ export function registerExocomWorkTools(pi: ExtensionAPI, deps: ExocomWorkDeps):
|
|
|
191
178
|
renderCall(args, theme) {
|
|
192
179
|
return new Text(`${theme.fg("toolTitle", theme.bold("Exocom Answer "))}${theme.fg("accent", normalizeMetadataText(args.ask_id, 80, "ask"))}`, 0, 0);
|
|
193
180
|
},
|
|
194
|
-
renderResult
|
|
195
|
-
const first = res.content.find((item) => item.type === "text");
|
|
196
|
-
const text = first?.type === "text" ? first.text : "Exocom answer failed";
|
|
197
|
-
if (workToolFailed(res)) return new Text(theme.fg("error", text), 0, 0);
|
|
198
|
-
return new Text(theme.fg(expanded ? "toolOutput" : "accent", expanded ? text : `${text} (${keyHint("app.tools.expand", "to expand")})`), 0, 0);
|
|
199
|
-
},
|
|
181
|
+
renderResult: renderLiteralToolResult,
|
|
200
182
|
});
|
|
201
183
|
|
|
202
184
|
pi.registerTool({
|
|
@@ -221,12 +203,7 @@ export function registerExocomWorkTools(pi: ExtensionAPI, deps: ExocomWorkDeps):
|
|
|
221
203
|
renderCall(args, theme) {
|
|
222
204
|
return new Text(`${theme.fg("toolTitle", theme.bold("Exocom Decline "))}${theme.fg("accent", normalizeMetadataText(args.ask_id, 80, "ask"))}`, 0, 0);
|
|
223
205
|
},
|
|
224
|
-
renderResult
|
|
225
|
-
const first = res.content.find((item) => item.type === "text");
|
|
226
|
-
const text = first?.type === "text" ? first.text : "Exocom decline failed";
|
|
227
|
-
if (workToolFailed(res)) return new Text(theme.fg("error", text), 0, 0);
|
|
228
|
-
return new Text(theme.fg(expanded ? "toolOutput" : "accent", expanded ? text : `${text} (${keyHint("app.tools.expand", "to expand")})`), 0, 0);
|
|
229
|
-
},
|
|
206
|
+
renderResult: renderLiteralToolResult,
|
|
230
207
|
});
|
|
231
208
|
|
|
232
209
|
pi.registerTool({
|
|
@@ -250,11 +227,14 @@ export function registerExocomWorkTools(pi: ExtensionAPI, deps: ExocomWorkDeps):
|
|
|
250
227
|
renderCall(args, theme) {
|
|
251
228
|
return new Text(`${theme.fg("toolTitle", theme.bold("Exocom Wait "))}${theme.fg("accent", normalizeMetadataText(args.work_key, 80, "work"))}`, 0, 0);
|
|
252
229
|
},
|
|
253
|
-
renderResult(res,
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (
|
|
257
|
-
|
|
230
|
+
renderResult(res, options, theme, context) {
|
|
231
|
+
// A retained answer is peer prose, not the literal receipt for arming a wait.
|
|
232
|
+
// Its ok=false can be a valid negative answer; only the host error flag is failure.
|
|
233
|
+
if (options.expanded && !(context?.isError ?? res.isError) && res.details?.status === "answered") {
|
|
234
|
+
const text = res.content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
|
|
235
|
+
return createReportBody(text, theme, {}, context?.lastComponent);
|
|
236
|
+
}
|
|
237
|
+
return renderLiteralToolResult(res, options, theme, context);
|
|
258
238
|
},
|
|
259
239
|
});
|
|
260
240
|
|
|
@@ -274,12 +254,7 @@ export function registerExocomWorkTools(pi: ExtensionAPI, deps: ExocomWorkDeps):
|
|
|
274
254
|
renderCall(args, theme) {
|
|
275
255
|
return new Text(`${theme.fg("toolTitle", theme.bold("Exocom Release "))}${theme.fg("accent", normalizeMetadataText(args.work_key, 80, "work"))}`, 0, 0);
|
|
276
256
|
},
|
|
277
|
-
renderResult
|
|
278
|
-
const first = res.content.find((item) => item.type === "text");
|
|
279
|
-
const text = first?.type === "text" ? first.text : "Exocom release failed";
|
|
280
|
-
if (workToolFailed(res)) return new Text(theme.fg("error", text), 0, 0);
|
|
281
|
-
return new Text(theme.fg(expanded ? "toolOutput" : "accent", expanded ? text : `${text} (${keyHint("app.tools.expand", "to expand")})`), 0, 0);
|
|
282
|
-
},
|
|
257
|
+
renderResult: renderLiteralToolResult,
|
|
283
258
|
});
|
|
284
259
|
|
|
285
260
|
pi.registerTool({
|
|
@@ -298,12 +273,7 @@ export function registerExocomWorkTools(pi: ExtensionAPI, deps: ExocomWorkDeps):
|
|
|
298
273
|
renderCall(args, theme) {
|
|
299
274
|
return new Text(`${theme.fg("toolTitle", theme.bold("Exocom Progress "))}${theme.fg("accent", normalizeMetadataText(args.work_key, 80, "work"))}`, 0, 0);
|
|
300
275
|
},
|
|
301
|
-
renderResult
|
|
302
|
-
const first = res.content.find((item) => item.type === "text");
|
|
303
|
-
const text = first?.type === "text" ? first.text : "Exocom progress failed";
|
|
304
|
-
if (workToolFailed(res)) return new Text(theme.fg("error", text), 0, 0);
|
|
305
|
-
return new Text(theme.fg(expanded ? "toolOutput" : "accent", expanded ? text : `${text} (${keyHint("app.tools.expand", "to expand")})`), 0, 0);
|
|
306
|
-
},
|
|
276
|
+
renderResult: renderLiteralToolResult,
|
|
307
277
|
});
|
|
308
278
|
|
|
309
279
|
pi.registerTool({
|
|
@@ -318,11 +288,6 @@ export function registerExocomWorkTools(pi: ExtensionAPI, deps: ExocomWorkDeps):
|
|
|
318
288
|
renderCall(_args, theme) {
|
|
319
289
|
return new Text(theme.fg("toolTitle", theme.bold("Exocom Status")), 0, 0);
|
|
320
290
|
},
|
|
321
|
-
renderResult
|
|
322
|
-
const first = res.content.find((item) => item.type === "text");
|
|
323
|
-
const text = first?.type === "text" ? first.text : "Exocom status failed";
|
|
324
|
-
if (workToolFailed(res)) return new Text(theme.fg("error", text), 0, 0);
|
|
325
|
-
return new Text(theme.fg(expanded ? "toolOutput" : "accent", expanded ? text : `${text} (${keyHint("app.tools.expand", "to expand")})`), 0, 0);
|
|
326
|
-
},
|
|
291
|
+
renderResult: renderLiteralToolResult,
|
|
327
292
|
});
|
|
328
293
|
}
|