@inhuman.tools/harness-sdk 0.0.0-stage → 0.2.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/LICENSE.md +7 -0
- package/dist/client-agents-tree.d.ts +123 -0
- package/dist/client-agents-tree.d.ts.map +1 -0
- package/dist/client-agents-tree.js +504 -0
- package/dist/client-agents-tree.js.map +1 -0
- package/dist/client-agents.d.ts +50 -0
- package/dist/client-agents.d.ts.map +1 -0
- package/dist/client-agents.js +299 -0
- package/dist/client-agents.js.map +1 -0
- package/dist/client.d.ts +89 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +589 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/launch/continue-pick.d.ts +17 -0
- package/dist/launch/continue-pick.d.ts.map +1 -0
- package/dist/launch/continue-pick.js +20 -0
- package/dist/launch/continue-pick.js.map +1 -0
- package/dist/launch/index.d.ts +6 -0
- package/dist/launch/index.d.ts.map +1 -0
- package/dist/launch/index.js +6 -0
- package/dist/launch/index.js.map +1 -0
- package/dist/launch/model-flags.d.ts +44 -0
- package/dist/launch/model-flags.d.ts.map +1 -0
- package/dist/launch/model-flags.js +216 -0
- package/dist/launch/model-flags.js.map +1 -0
- package/dist/launch/streams.d.ts +15 -0
- package/dist/launch/streams.d.ts.map +1 -0
- package/dist/launch/streams.js +2 -0
- package/dist/launch/streams.js.map +1 -0
- package/dist/launch/term-continue.d.ts +13 -0
- package/dist/launch/term-continue.d.ts.map +1 -0
- package/dist/launch/term-continue.js +17 -0
- package/dist/launch/term-continue.js.map +1 -0
- package/dist/launch/term-session-key.d.ts +8 -0
- package/dist/launch/term-session-key.d.ts.map +1 -0
- package/dist/launch/term-session-key.js +76 -0
- package/dist/launch/term-session-key.js.map +1 -0
- package/dist/observable-abort-signal.d.ts +7 -0
- package/dist/observable-abort-signal.d.ts.map +1 -0
- package/dist/observable-abort-signal.js +56 -0
- package/dist/observable-abort-signal.js.map +1 -0
- package/dist/open-external.d.ts +2 -0
- package/dist/open-external.d.ts.map +1 -0
- package/dist/open-external.js +24 -0
- package/dist/open-external.js.map +1 -0
- package/dist/paths.d.ts +7 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +16 -0
- package/dist/paths.js.map +1 -0
- package/dist/text/agent-cover.d.ts +43 -0
- package/dist/text/agent-cover.d.ts.map +1 -0
- package/dist/text/agent-cover.js +123 -0
- package/dist/text/agent-cover.js.map +1 -0
- package/dist/text/format-shell.d.ts +5 -0
- package/dist/text/format-shell.d.ts.map +1 -0
- package/dist/text/format-shell.js +53 -0
- package/dist/text/format-shell.js.map +1 -0
- package/dist/text/identity.d.ts +67 -0
- package/dist/text/identity.d.ts.map +1 -0
- package/dist/text/identity.js +433 -0
- package/dist/text/identity.js.map +1 -0
- package/dist/text/index.d.ts +14 -0
- package/dist/text/index.d.ts.map +1 -0
- package/dist/text/index.js +15 -0
- package/dist/text/index.js.map +1 -0
- package/dist/text/liveness.d.ts +62 -0
- package/dist/text/liveness.d.ts.map +1 -0
- package/dist/text/liveness.js +270 -0
- package/dist/text/liveness.js.map +1 -0
- package/dist/text/mcp-identity.d.ts +25 -0
- package/dist/text/mcp-identity.d.ts.map +1 -0
- package/dist/text/mcp-identity.js +133 -0
- package/dist/text/mcp-identity.js.map +1 -0
- package/dist/text/pretty-identity.d.ts +15 -0
- package/dist/text/pretty-identity.d.ts.map +1 -0
- package/dist/text/pretty-identity.js +112 -0
- package/dist/text/pretty-identity.js.map +1 -0
- package/dist/text/sugarcoat.d.ts +29 -0
- package/dist/text/sugarcoat.d.ts.map +1 -0
- package/dist/text/sugarcoat.js +148 -0
- package/dist/text/sugarcoat.js.map +1 -0
- package/dist/text/unwrap-program.d.ts +2 -0
- package/dist/text/unwrap-program.d.ts.map +1 -0
- package/dist/text/unwrap-program.js +137 -0
- package/dist/text/unwrap-program.js.map +1 -0
- package/dist/text/work-text.d.ts +72 -0
- package/dist/text/work-text.d.ts.map +1 -0
- package/dist/text/work-text.js +337 -0
- package/dist/text/work-text.js.map +1 -0
- package/dist/transport/unix.d.ts +19 -0
- package/dist/transport/unix.d.ts.map +1 -0
- package/dist/transport/unix.js +68 -0
- package/dist/transport/unix.js.map +1 -0
- package/package.json +79 -3
- package/src/client-agents-tree.ts +642 -0
- package/src/client-agents.ts +230 -0
- package/src/client.ts +686 -0
- package/src/index.ts +27 -0
- package/src/launch/continue-pick.ts +28 -0
- package/src/launch/index.ts +5 -0
- package/src/launch/model-flags.ts +273 -0
- package/src/launch/streams.ts +13 -0
- package/src/launch/term-continue.ts +31 -0
- package/src/launch/term-session-key.ts +70 -0
- package/src/observable-abort-signal.ts +66 -0
- package/src/open-external.ts +22 -0
- package/src/paths.ts +20 -0
- package/src/text/agent-cover.ts +136 -0
- package/src/text/format-shell.ts +52 -0
- package/src/text/identity.ts +473 -0
- package/src/text/index.ts +29 -0
- package/src/text/liveness.ts +322 -0
- package/src/text/mcp-identity.ts +148 -0
- package/src/text/pretty-identity.ts +120 -0
- package/src/text/sugarcoat.ts +165 -0
- package/src/text/unwrap-program.ts +133 -0
- package/src/text/work-text.ts +388 -0
- package/src/transport/unix.ts +84 -0
- package/README.md +0 -3
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display-only shfmt reprint of a shell command. Never rewrite argsJson.
|
|
3
|
+
* sh-syntax instantiates WASM per print and is async — cache the result;
|
|
4
|
+
* parse failure is identity. File print's trailing newline is stripped.
|
|
5
|
+
*/
|
|
6
|
+
import { observable, runInAction } from "mobx";
|
|
7
|
+
import { print } from "sh-syntax";
|
|
8
|
+
|
|
9
|
+
const generation = observable.box(0);
|
|
10
|
+
const cache = new Map<string, string>();
|
|
11
|
+
const pending = new Map<string, Promise<void>>();
|
|
12
|
+
|
|
13
|
+
export function shellFormatGeneration(): number {
|
|
14
|
+
return generation.get();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Cached reprint, or `src` until WASM print settles / if parse fails. */
|
|
18
|
+
export function formatShellCommand(src: string): string {
|
|
19
|
+
const hit = cache.get(src);
|
|
20
|
+
if (hit !== undefined) return hit;
|
|
21
|
+
generation.get(); // MobX subscribe — WASM settle increments this so reprint rerenders. Not a dead call.
|
|
22
|
+
schedule(src);
|
|
23
|
+
return src;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function formatShellCommandReady(src: string): Promise<string> {
|
|
27
|
+
const hit = cache.get(src);
|
|
28
|
+
if (hit !== undefined) return hit;
|
|
29
|
+
schedule(src);
|
|
30
|
+
await pending.get(src);
|
|
31
|
+
return cache.get(src) ?? src;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function schedule(src: string): void {
|
|
35
|
+
if (src === "" || pending.has(src) || cache.has(src)) return;
|
|
36
|
+
const work = (async () => {
|
|
37
|
+
try {
|
|
38
|
+
const out = await print(src);
|
|
39
|
+
const formatted = out.replaceAll(/\n+$/g, "");
|
|
40
|
+
const next = formatted === "" ? src : formatted;
|
|
41
|
+
cache.set(src, next);
|
|
42
|
+
if (next !== src) {
|
|
43
|
+
runInAction(() => generation.set(generation.get() + 1));
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
cache.set(src, src);
|
|
47
|
+
} finally {
|
|
48
|
+
pending.delete(src);
|
|
49
|
+
}
|
|
50
|
+
})();
|
|
51
|
+
pending.set(src, work);
|
|
52
|
+
}
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity line from argsJson. Never a file count. No duration.
|
|
3
|
+
* Scheme outer is sugarcoat of expr(); tools use .name + args.
|
|
4
|
+
* Shell tape skips pretty-mcp `run`/`ran` — gutter `$`, bash command
|
|
5
|
+
* (or optional `description` on the collapsed row). A cwd other than `.`
|
|
6
|
+
* is the line after that call, not a right column. No `exit N` — fail
|
|
7
|
+
* is the gutter. Command display is an shfmt reprint; argsJson is what runs.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
AgentCallJob,
|
|
11
|
+
CallJob,
|
|
12
|
+
AgentMessageJob,
|
|
13
|
+
type KernelRoot,
|
|
14
|
+
MCPToolJob,
|
|
15
|
+
ProgrammaticJob,
|
|
16
|
+
FileMutateToolJob,
|
|
17
|
+
ToolCallJob,
|
|
18
|
+
TurnNotificationChunk,
|
|
19
|
+
BotCallJob,
|
|
20
|
+
} from "@inhuman.tools/harness-model";
|
|
21
|
+
import { renderMustache, splitIdentity } from "@inhuman.tools/pretty-mcp";
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
agentCoverVerb,
|
|
25
|
+
formatAgentCoverAnnotation,
|
|
26
|
+
formatAskAgentWho,
|
|
27
|
+
type CoverAskContext as AgentCoverAskContext,
|
|
28
|
+
} from "./agent-cover.js";
|
|
29
|
+
import { formatShellCommand } from "./format-shell.js";
|
|
30
|
+
import { mcpIdentity } from "./mcp-identity.js";
|
|
31
|
+
import { prettyIdentity, stripQuestion } from "./pretty-identity.js";
|
|
32
|
+
import { sugarcoatProgram } from "./sugarcoat.js";
|
|
33
|
+
|
|
34
|
+
export type CoverAskContext = AgentCoverAskContext & {
|
|
35
|
+
readonly kernel?: KernelRoot | null;
|
|
36
|
+
/** Recipe name when a worker tool (not the spawn) sits on the host cover. */
|
|
37
|
+
readonly agentSpec?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Wire name of the scheme-repl bag. Same string as SCHEME_REPL_TOOL_NAME. */
|
|
41
|
+
export const SCHEME_REPL_NAME = "scheme_repl";
|
|
42
|
+
|
|
43
|
+
/** Wire name of the scheme-watch bag. Same string as SCHEME_WATCH_TOOL_NAME. */
|
|
44
|
+
export const SCHEME_WATCH_NAME = "scheme_watch";
|
|
45
|
+
|
|
46
|
+
/** Collapsed caption XOR the program. Both bags, not only the repl. */
|
|
47
|
+
export function isSchemeBagName(name: string): boolean {
|
|
48
|
+
return name === SCHEME_REPL_NAME || name === SCHEME_WATCH_NAME;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type IdentityLine = {
|
|
52
|
+
readonly name: string;
|
|
53
|
+
readonly object: string;
|
|
54
|
+
readonly status: string;
|
|
55
|
+
/** Collapsed tape token. Command / expr (`object`) is on expand. */
|
|
56
|
+
readonly caption?: string;
|
|
57
|
+
/** pretty-mcp telegram is the whole row; `name` stays the wire name. */
|
|
58
|
+
readonly pretty?: boolean;
|
|
59
|
+
readonly block?: string;
|
|
60
|
+
readonly agent?: boolean;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const CAPTION_MAX_CODE_POINTS = 80;
|
|
64
|
+
|
|
65
|
+
/** TUI emits `…`. sdk glance `clampChars` does not — sharing the helper drops the ellipsis. */
|
|
66
|
+
export function captionFromArgs(args: Record<string, unknown> | null): string | undefined {
|
|
67
|
+
if (args === null) return undefined;
|
|
68
|
+
const raw = args.description;
|
|
69
|
+
if (typeof raw !== "string") return undefined;
|
|
70
|
+
const flat = raw.replaceAll(/\s+/g, " ").trim();
|
|
71
|
+
if (flat === "") return undefined;
|
|
72
|
+
const points = [...flat];
|
|
73
|
+
if (points.length <= CAPTION_MAX_CODE_POINTS) return flat;
|
|
74
|
+
return `${points.slice(0, CAPTION_MAX_CODE_POINTS - 1).join("")}…`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function tailEllipsis(path: string, width: number): string {
|
|
78
|
+
if (width < 2) return "…";
|
|
79
|
+
if (path.length <= width) return path;
|
|
80
|
+
const slash = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
|
|
81
|
+
const file = slash >= 0 ? path.slice(slash + 1) : path;
|
|
82
|
+
const marked = slash >= 0 ? `/${file}` : file;
|
|
83
|
+
if (marked.length + 1 <= width) return `…${marked}`;
|
|
84
|
+
return `…${file.slice(-(width - 1))}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Replica strings can be `null` while a room is still settling. */
|
|
88
|
+
function asText(value: unknown): string {
|
|
89
|
+
return typeof value === "string" ? value : "";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function stringArg(args: Readonly<Record<string, unknown>> | null, key: string): string | undefined {
|
|
93
|
+
if (args === null) return undefined;
|
|
94
|
+
const value = args[key];
|
|
95
|
+
return typeof value === "string" && value !== "" ? value : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Token the model wrote. Relative keeps `./` / `../`; `.` is omitted. */
|
|
99
|
+
function shellCwdToken(args: Readonly<Record<string, unknown>> | null): string {
|
|
100
|
+
const cwd = stringArg(args, "cwd");
|
|
101
|
+
if (cwd === undefined || cwd === ".") return "";
|
|
102
|
+
if (cwd.startsWith("/") || cwd.startsWith("\\") || cwd.startsWith(".")) return cwd;
|
|
103
|
+
return `./${cwd}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function fallbackObject(work: ToolCallJob): string {
|
|
107
|
+
const invoked = work.invocation();
|
|
108
|
+
const args = work.args;
|
|
109
|
+
if (args == null) return invoked.length > 48 ? `${invoked.slice(0, 47)}…` : invoked;
|
|
110
|
+
const raw = JSON.stringify(args);
|
|
111
|
+
if (invoked !== raw) return invoked;
|
|
112
|
+
return raw.length > 48 ? `${raw.slice(0, 47)}…` : raw;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function isPathShapedName(name: string): boolean {
|
|
116
|
+
return name === "read_file" || name === "list_dir" || name === "write_file" || name === "edit_file";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** File mutations paint as open Diff blocks; identity is the path, not a telegram. */
|
|
120
|
+
export function isMutateName(name: string): boolean {
|
|
121
|
+
return name === "edit_file" || name === "write_file";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* True when edit/create args would paint a Diff (pin or sealed).
|
|
126
|
+
* `write_file` that existed without stored prior text is not a create Diff —
|
|
127
|
+
* same cut as `writeFileDiff` in work-cells. A prior held in a blob is stored:
|
|
128
|
+
* its text arrives after the document names it.
|
|
129
|
+
*/
|
|
130
|
+
export function mutateArgsHaveDiff(work: ToolCallJob): boolean {
|
|
131
|
+
if (!isMutateName(work.name)) return false;
|
|
132
|
+
const args = work.args;
|
|
133
|
+
if (work.name === "edit_file") {
|
|
134
|
+
return argString(args, "old_string") !== "" || argString(args, "new_string") !== "";
|
|
135
|
+
}
|
|
136
|
+
if (argString(args, "content") === "") return false;
|
|
137
|
+
if (work instanceof FileMutateToolJob && work.existed === true && work.prior === null) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function argString(args: Record<string, unknown> | null, key: string): string {
|
|
144
|
+
const value = args?.[key];
|
|
145
|
+
return typeof value === "string" ? value : "";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function flattenLine(text: string): string {
|
|
149
|
+
return text.replaceAll(/\s+/g, " ").trim();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function prettySentence(
|
|
153
|
+
name: string,
|
|
154
|
+
object: string,
|
|
155
|
+
extras: { readonly caption?: string; readonly block?: string } = {},
|
|
156
|
+
): IdentityLine {
|
|
157
|
+
const caption = extras.caption !== undefined && extras.caption !== "" ? extras.caption : undefined;
|
|
158
|
+
const block = extras.block !== undefined && extras.block !== "" ? extras.block : undefined;
|
|
159
|
+
return {
|
|
160
|
+
name: asText(name),
|
|
161
|
+
object: stripQuestion(asText(object)),
|
|
162
|
+
status: "",
|
|
163
|
+
pretty: true,
|
|
164
|
+
...(caption === undefined ? {} : { caption }),
|
|
165
|
+
...(block === undefined ? {} : { block }),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Args plus sealed outcome object. Sentences stay on the plugin face. */
|
|
170
|
+
function pluginFaceBag(work: ToolCallJob): Record<string, unknown> {
|
|
171
|
+
const args = work.args ?? {};
|
|
172
|
+
const bag: Record<string, unknown> = { ...args };
|
|
173
|
+
const snap = work.outcomeSnap;
|
|
174
|
+
const value = snap?.ok === true ? snap.value : undefined;
|
|
175
|
+
if (value !== null && value !== undefined && typeof value === "object" && !Array.isArray(value)) {
|
|
176
|
+
for (const [key, field] of Object.entries(value)) {
|
|
177
|
+
const prior = bag[key];
|
|
178
|
+
if (prior !== undefined && prior !== "") continue;
|
|
179
|
+
if (field === undefined || field === "") continue;
|
|
180
|
+
bag[key] = field;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
for (const key of Object.keys(bag)) {
|
|
184
|
+
const text = bag[key];
|
|
185
|
+
if (typeof text === "string") bag[key] = flattenLine(text);
|
|
186
|
+
}
|
|
187
|
+
return bag;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function pluginFaceIdentity(
|
|
191
|
+
work: ToolCallJob | ProgrammaticJob,
|
|
192
|
+
kernel?: KernelRoot | null,
|
|
193
|
+
): IdentityLine | null {
|
|
194
|
+
if (kernel == null) return null;
|
|
195
|
+
if (work instanceof ProgrammaticJob) return null;
|
|
196
|
+
const name = asText(work.name);
|
|
197
|
+
if (name === "") return null;
|
|
198
|
+
const spec = kernel.pluginTool(name);
|
|
199
|
+
if (spec == null || (spec.question == null && spec.effect == null)) return null;
|
|
200
|
+
const bag = pluginFaceBag(work);
|
|
201
|
+
const sealed = work.completed || work.cancelled === true;
|
|
202
|
+
const template = sealed && spec.effect != null ? spec.effect : (spec.question ?? spec.effect);
|
|
203
|
+
if (template == null) return null;
|
|
204
|
+
const markdown = renderMustache(template, bag).replace(/\n+$/, "");
|
|
205
|
+
if (markdown.trim() === "") return null;
|
|
206
|
+
const { line, block } = splitIdentity(markdown);
|
|
207
|
+
const caption = spec.caption == null ? undefined : flattenLine(renderMustache(spec.caption, bag));
|
|
208
|
+
return prettySentence(name, line, {
|
|
209
|
+
caption,
|
|
210
|
+
block,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Path + optional status. No wire name — the gutter already marks the row. */
|
|
215
|
+
export function formatMutateIdentity(line: Pick<IdentityLine, "object" | "status">, width: number): string {
|
|
216
|
+
const suffix = line.status === "" ? "" : ` ${line.status}`;
|
|
217
|
+
const budget = Math.max(4, width - suffix.length);
|
|
218
|
+
return `${tailEllipsis(line.object, budget)}${suffix}`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function withCaption(line: IdentityLine, caption: string | undefined): IdentityLine {
|
|
222
|
+
return caption === undefined ? line : { ...line, caption };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function identityLine(work: ToolCallJob | ProgrammaticJob, kernel?: KernelRoot | null): IdentityLine {
|
|
226
|
+
if (work instanceof ToolCallJob && work.name === "shell") {
|
|
227
|
+
const args = work.args;
|
|
228
|
+
const command = stringArg(args, "command");
|
|
229
|
+
return withCaption(
|
|
230
|
+
{
|
|
231
|
+
name: "shell",
|
|
232
|
+
object: command === undefined ? fallbackObject(work) : formatShellCommand(command),
|
|
233
|
+
status: shellCwdToken(args),
|
|
234
|
+
},
|
|
235
|
+
captionFromArgs(args),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
const pluginFace = pluginFaceIdentity(work, kernel);
|
|
239
|
+
if (pluginFace !== null) return pluginFace;
|
|
240
|
+
if (work instanceof ProgrammaticJob) {
|
|
241
|
+
const args = work.args;
|
|
242
|
+
return withCaption(
|
|
243
|
+
{
|
|
244
|
+
name: asText(work.toolName),
|
|
245
|
+
object: sugarcoatProgram(work.expr),
|
|
246
|
+
status: "",
|
|
247
|
+
},
|
|
248
|
+
captionFromArgs(args),
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
const pretty = prettyIdentity(work, work.completed || work.cancelled === true ? "effect" : "question");
|
|
252
|
+
if (work instanceof MCPToolJob) {
|
|
253
|
+
const mcp = mcpIdentity(work);
|
|
254
|
+
if (pretty !== null) {
|
|
255
|
+
return {
|
|
256
|
+
name: asText(mcp.server),
|
|
257
|
+
object: stripQuestion(asText(pretty.line)),
|
|
258
|
+
status: "",
|
|
259
|
+
pretty: true,
|
|
260
|
+
block: pretty.block,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
return { name: asText(mcp.server), object: asText(mcp.action), status: "" };
|
|
264
|
+
}
|
|
265
|
+
if (work instanceof CallJob) {
|
|
266
|
+
return {
|
|
267
|
+
name: agentCallName(work),
|
|
268
|
+
object: "",
|
|
269
|
+
status: "",
|
|
270
|
+
agent: true,
|
|
271
|
+
block: agentSpawnPrompt(work),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
const args = work.args;
|
|
275
|
+
const name = asText(work.name);
|
|
276
|
+
let object: string;
|
|
277
|
+
const status = "";
|
|
278
|
+
switch (name) {
|
|
279
|
+
case "read_file":
|
|
280
|
+
case "list_dir":
|
|
281
|
+
case "write_file":
|
|
282
|
+
case "edit_file":
|
|
283
|
+
object = stringArg(args, "path") ?? fallbackObject(work);
|
|
284
|
+
break;
|
|
285
|
+
case "glob":
|
|
286
|
+
case "fff_search": {
|
|
287
|
+
const head = stringArg(args, name === "glob" ? "pattern" : "query") ?? fallbackObject(work);
|
|
288
|
+
const cwd = searchCwd(args);
|
|
289
|
+
object = cwd === undefined ? head : `${head} ${cwd}`;
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
case "grep": {
|
|
293
|
+
const pattern = stringArg(args, "pattern") ?? fallbackObject(work);
|
|
294
|
+
const path = stringArg(args, "path");
|
|
295
|
+
object = path === undefined ? pattern : `${pattern} ${path}`;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
default: {
|
|
299
|
+
let hit: string | undefined;
|
|
300
|
+
for (const key of ["path", "uri", "command", "query", "pattern"]) {
|
|
301
|
+
hit = stringArg(args, key);
|
|
302
|
+
if (hit !== undefined) break;
|
|
303
|
+
}
|
|
304
|
+
if (hit === undefined && args !== null) {
|
|
305
|
+
for (const value of Object.values(args)) {
|
|
306
|
+
if (typeof value === "string" && value !== "") {
|
|
307
|
+
hit = value;
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
object = hit ?? fallbackObject(work);
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (isMutateName(name)) {
|
|
317
|
+
return withCaption({ name, object, status }, captionFromArgs(args));
|
|
318
|
+
}
|
|
319
|
+
if (pretty !== null) {
|
|
320
|
+
return { name, object: stripQuestion(pretty.line), status, pretty: true, block: pretty.block };
|
|
321
|
+
}
|
|
322
|
+
return { name, object, status };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function formatIdentity(line: IdentityLine, width: number, pathShaped: boolean): string {
|
|
326
|
+
const gap = line.status === "" ? `${line.name} ` : `${line.name} `;
|
|
327
|
+
const suffix = line.status === "" ? "" : ` ${line.status}`;
|
|
328
|
+
const budget = Math.max(4, width - gap.length - suffix.length);
|
|
329
|
+
const object = pathShaped ? tailEllipsis(line.object, budget) : line.object;
|
|
330
|
+
return `${gap}${object}${suffix}`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** glob / fff_search search root. `cwd` wins; `path` is the ingest alias. */
|
|
334
|
+
function searchCwd(args: Record<string, unknown> | null): string | undefined {
|
|
335
|
+
return stringArg(args, "cwd") ?? stringArg(args, "path");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** `**token**` closes early if the token itself contains `*` (glob stars). */
|
|
339
|
+
function emphasize(token: string): string {
|
|
340
|
+
return token.includes("*") ? token : `**${token}**`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function coverPathVerb(verb: string, args: Record<string, unknown> | null): string | undefined {
|
|
344
|
+
const path = stringArg(args, "path");
|
|
345
|
+
if (path === undefined) return undefined;
|
|
346
|
+
const slash = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
|
|
347
|
+
const file = slash >= 0 ? path.slice(slash + 1) : path;
|
|
348
|
+
if (file === "") return undefined;
|
|
349
|
+
return `${verb} ${emphasize(file)}`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function agentCallName(work: CallJob): string {
|
|
353
|
+
const spec = stringArg(work.args, "spec")?.trim() ?? "";
|
|
354
|
+
if (spec !== "") return spec;
|
|
355
|
+
const bot = work instanceof BotCallJob ? asText(work.botName).trim() || asText(work.contributionBotName).trim() : "";
|
|
356
|
+
if (bot !== "") return bot;
|
|
357
|
+
const face = work instanceof AgentCallJob ? (work.faceName ?? "").trim() : "";
|
|
358
|
+
if (face !== "" && face !== "anonymous") return face;
|
|
359
|
+
return "agent";
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function agentSpawnPrompt(work: CallJob): string {
|
|
363
|
+
return stringArg(work.args, "message") ?? "";
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function notificationPaintText(chunk: TurnNotificationChunk): string {
|
|
367
|
+
const lines = chunk.notifications.map((note) => {
|
|
368
|
+
const home = note.parent;
|
|
369
|
+
if (home instanceof CallJob) return `from ${agentCallName(home)}: ${note.message}`;
|
|
370
|
+
const text = note.message.trim();
|
|
371
|
+
return text === "" ? "" : text;
|
|
372
|
+
});
|
|
373
|
+
return lines.filter((line) => line !== "").join("\n");
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Successful cutoff, or the human notice on a failed CompactJob. */
|
|
377
|
+
export function compactJobPaintText(notice: string, focus: string, failed: boolean): string {
|
|
378
|
+
if (failed) {
|
|
379
|
+
const text = notice.trim();
|
|
380
|
+
return text === "" ? "couldn't compact — try again later" : text;
|
|
381
|
+
}
|
|
382
|
+
const trimmed = focus.trim();
|
|
383
|
+
return trimmed === "" ? "compacted" : `compacted · ${trimmed}`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Isolated-cover rim (omnibox top-right). Markdown: furniture + `**bright**`
|
|
388
|
+
* (chrome lifts emphasis to primary). MCP is server + action. Shell is
|
|
389
|
+
* `run command` plus the cwd token when set — the command itself is the
|
|
390
|
+
* composer stack, not this slot. Agent send/ask is dim `ask`/`send` + bright
|
|
391
|
+
* `{model} {Spec} agent`; the prompt rides the stack. A worker tool on the
|
|
392
|
+
* host cover prefixes `**{Spec} agent** ·` so the who is not the host.
|
|
393
|
+
*/
|
|
394
|
+
export function coverAskAnnotation(work: object | null | undefined, ctx?: CoverAskContext): string | undefined {
|
|
395
|
+
if (work instanceof CallJob) {
|
|
396
|
+
return formatAgentCoverAnnotation({
|
|
397
|
+
verb: agentCoverVerb(work),
|
|
398
|
+
model: ctx?.model ?? "",
|
|
399
|
+
spec: agentCallName(work),
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
if (work instanceof AgentMessageJob) {
|
|
403
|
+
const args = work.args;
|
|
404
|
+
const target = stringArg(args, "targetName") ?? stringArg(args, "targetId") ?? "";
|
|
405
|
+
return formatAgentCoverAnnotation({
|
|
406
|
+
verb: "message",
|
|
407
|
+
model: "",
|
|
408
|
+
spec: target,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
return prefixWorkerAsk(ctx?.agentSpec, coverAskToolAnnotation(work, ctx));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function coverAskToolAnnotation(work: object | null | undefined, ctx?: CoverAskContext): string | undefined {
|
|
415
|
+
if (work instanceof ToolCallJob && work.name === "shell") {
|
|
416
|
+
const cwd = shellCwdToken(work.args);
|
|
417
|
+
return cwd === "" ? "run command" : `run command ${cwd}`;
|
|
418
|
+
}
|
|
419
|
+
if (work instanceof MCPToolJob || work instanceof ToolCallJob || work instanceof ProgrammaticJob) {
|
|
420
|
+
const pluginFace = pluginFaceIdentity(work, ctx?.kernel);
|
|
421
|
+
if (pluginFace !== null) return pluginFace.object;
|
|
422
|
+
const pretty = prettyIdentity(work, "question");
|
|
423
|
+
if (pretty !== null) return stripQuestion(pretty.line);
|
|
424
|
+
}
|
|
425
|
+
if (work instanceof MCPToolJob) {
|
|
426
|
+
const mcp = mcpIdentity(work);
|
|
427
|
+
if (mcp.action === "") return undefined;
|
|
428
|
+
return `${mcp.server} ${emphasize(mcp.action)}`;
|
|
429
|
+
}
|
|
430
|
+
if (!(work instanceof ToolCallJob)) return undefined;
|
|
431
|
+
const args = work.args;
|
|
432
|
+
switch (work.name) {
|
|
433
|
+
case "edit_file":
|
|
434
|
+
return coverPathVerb("edit", args);
|
|
435
|
+
case "write_file":
|
|
436
|
+
return coverPathVerb(work instanceof FileMutateToolJob && work.existed === true ? "update" : "create", args);
|
|
437
|
+
case "read_file":
|
|
438
|
+
return coverPathVerb("Read", args);
|
|
439
|
+
case "list_dir":
|
|
440
|
+
return coverPathVerb("list", args);
|
|
441
|
+
case "grep": {
|
|
442
|
+
const pattern = stringArg(args, "pattern");
|
|
443
|
+
return pattern === undefined ? undefined : `grep ${emphasize(pattern)}`;
|
|
444
|
+
}
|
|
445
|
+
case "glob": {
|
|
446
|
+
const pattern = stringArg(args, "pattern");
|
|
447
|
+
if (pattern === undefined) return undefined;
|
|
448
|
+
const cwd = searchCwd(args);
|
|
449
|
+
return cwd === undefined ? `glob ${emphasize(pattern)}` : `glob ${emphasize(pattern)} ${cwd}`;
|
|
450
|
+
}
|
|
451
|
+
case "fff_search": {
|
|
452
|
+
const query = stringArg(args, "query");
|
|
453
|
+
if (query === undefined) return undefined;
|
|
454
|
+
const cwd = searchCwd(args);
|
|
455
|
+
return cwd === undefined ? `search ${emphasize(query)}` : `search ${emphasize(query)} ${cwd}`;
|
|
456
|
+
}
|
|
457
|
+
default:
|
|
458
|
+
return undefined;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function prefixWorkerAsk(spec: string | undefined, asked: string | undefined): string | undefined {
|
|
463
|
+
if (spec === undefined) return asked;
|
|
464
|
+
const who = emphasize(formatAskAgentWho(spec));
|
|
465
|
+
if (asked == null || asked === "") return who;
|
|
466
|
+
return `${who} · ${asked}`;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function shellAskCommand(work: object | null | undefined): string | undefined {
|
|
470
|
+
if (!(work instanceof ToolCallJob) || work.name !== "shell") return undefined;
|
|
471
|
+
const command = stringArg(work.args, "command");
|
|
472
|
+
return command === undefined ? undefined : formatShellCommand(command);
|
|
473
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plain-text projections of the tape: identity lines, work-cell faces, Scheme receipts, liveness.
|
|
3
|
+
* The TUI paints from these and the headless eval client prints them. No quickdraw.
|
|
4
|
+
*/
|
|
5
|
+
export * from "./format-shell.js";
|
|
6
|
+
export * from "./identity.js";
|
|
7
|
+
export * from "./liveness.js";
|
|
8
|
+
export * from "./mcp-identity.js";
|
|
9
|
+
export * from "./pretty-identity.js";
|
|
10
|
+
export * from "./sugarcoat.js";
|
|
11
|
+
export * from "./unwrap-program.js";
|
|
12
|
+
export * from "./work-text.js";
|
|
13
|
+
|
|
14
|
+
// `identity.ts` widens the agent-cover context under the same name, so agent-cover is exported by name.
|
|
15
|
+
export {
|
|
16
|
+
agentAskStackedBody,
|
|
17
|
+
agentCoverPrompt,
|
|
18
|
+
agentCoverVerb,
|
|
19
|
+
agentExpandBlock,
|
|
20
|
+
agentRecipeDescription,
|
|
21
|
+
agentSpecKey,
|
|
22
|
+
formatAgentCoverAnnotation,
|
|
23
|
+
formatAskAgentWho,
|
|
24
|
+
formatWaitingForAgents,
|
|
25
|
+
STDLIB_AGENT_DESCRIPTIONS,
|
|
26
|
+
titleAgentSpec,
|
|
27
|
+
type AgentCoverVerb,
|
|
28
|
+
type CoverAskContext as AgentCoverAskContext,
|
|
29
|
+
} from "./agent-cover.js";
|