@vzlentin/harness 0.3.1 → 0.4.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/README.md +13 -11
- package/dist/cli.js +12 -1
- package/dist/setup.d.ts +6 -6
- package/dist/setup.js +28 -23
- package/package.json +1 -2
- package/scripts/postinstall.js +4 -4
- package/pi/attachment.ts +0 -303
- package/pi/chat.ts +0 -286
- package/pi/index.ts +0 -170
- package/pi/mirror.ts +0 -218
- package/pi/model-sync.ts +0 -102
- package/pi/package.json +0 -15
- package/pi/program-status.ts +0 -66
- package/pi/usage-line.ts +0 -59
package/pi/model-sync.ts
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The model and the thinking level of a mirror session. The agent in the host is the truth: the Pi session shows the
|
|
3
|
-
* agent's model and level, so Pi's footer and `/model` show them, and a change that the user makes in Pi is a request
|
|
4
|
-
* to the host. When the host refuses it, Pi goes back to the agent's model and level.
|
|
5
|
-
*/
|
|
6
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import type { Mirror, MirrorConfigure } from "../dist/mirror.js";
|
|
8
|
-
import type { ChildInfo } from "../dist/protocol.js";
|
|
9
|
-
|
|
10
|
-
/** What the sync uses of Pi's extension API. */
|
|
11
|
-
export type PiModels = Pick<ExtensionAPI, "setModel" | "getThinkingLevel" | "setThinkingLevel">;
|
|
12
|
-
|
|
13
|
-
/** What the sync uses of the context of the Pi session. */
|
|
14
|
-
export type PiSession = Pick<ExtensionContext, "model"> & {
|
|
15
|
-
readonly modelRegistry: Pick<ExtensionContext["modelRegistry"], "find">;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
/** What the sync uses of the mirror: the agent, and the requests that change it. */
|
|
19
|
-
export type MirroredAgent = Pick<Mirror, "child" | "configure">;
|
|
20
|
-
|
|
21
|
-
export class ModelSync {
|
|
22
|
-
// After a session switch, `#pi` and `#ctx` are stale and throw, so nothing uses them after `close()`.
|
|
23
|
-
readonly #pi: PiModels;
|
|
24
|
-
readonly #ctx: PiSession;
|
|
25
|
-
readonly #mirror: MirroredAgent;
|
|
26
|
-
readonly #notify: (cause: unknown) => void;
|
|
27
|
-
/** True while Pi sets the agent's model: `setModel` is async, and the agent changes often while it works. */
|
|
28
|
-
#settingModel = false;
|
|
29
|
-
#closed = false;
|
|
30
|
-
|
|
31
|
-
constructor(pi: PiModels, ctx: PiSession, mirror: MirroredAgent, notify: (cause: unknown) => void) {
|
|
32
|
-
this.#pi = pi;
|
|
33
|
-
this.#ctx = ctx;
|
|
34
|
-
this.#mirror = mirror;
|
|
35
|
-
this.#notify = notify;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Gives the Pi session the model and the thinking level of `child`, the agent as the host gives it. */
|
|
39
|
-
fromAgent(child: ChildInfo): void {
|
|
40
|
-
if (this.#closed) return;
|
|
41
|
-
const model = this.#ctx.modelRegistry.find(child.provider, child.model);
|
|
42
|
-
|
|
43
|
-
if (model !== undefined && !this.#piHasModel(child) && !this.#settingModel) {
|
|
44
|
-
this.#settingModel = true;
|
|
45
|
-
// Pi sets its own thinking level when it takes a model, so the agent's level follows once Pi has the model.
|
|
46
|
-
void this.#pi.setModel(model).finally(() => {
|
|
47
|
-
this.#settingModel = false;
|
|
48
|
-
const current = this.#mirror.child;
|
|
49
|
-
|
|
50
|
-
if (current !== undefined) this.#thinkingFromAgent(current);
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
this.#thinkingFromAgent(child);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Asks the host for the model that the user selected in Pi. */
|
|
58
|
-
piSelected(provider: string, model: string): void {
|
|
59
|
-
const child = this.#mirror.child;
|
|
60
|
-
|
|
61
|
-
if (child === undefined || (child.provider === provider && child.model === model)) return;
|
|
62
|
-
this.#configure({ provider, model });
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Asks the host for the thinking level that the user cycled to in Pi, once Pi has the agent's model. Pi also changes
|
|
67
|
-
* its level when it takes a model, so only the cycle key calls this, not each change of Pi's level.
|
|
68
|
-
*/
|
|
69
|
-
piCycled(): void {
|
|
70
|
-
const child = this.#mirror.child;
|
|
71
|
-
|
|
72
|
-
if (this.#closed || child === undefined || !this.#piHasModel(child)) return;
|
|
73
|
-
const thinking = this.#pi.getThinkingLevel();
|
|
74
|
-
|
|
75
|
-
if (child.thinking !== thinking) this.#configure({ thinking });
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
close(): void {
|
|
79
|
-
this.#closed = true;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
#configure(change: MirrorConfigure): void {
|
|
83
|
-
this.#mirror.configure(change).catch((cause: unknown) => {
|
|
84
|
-
this.#notify(cause);
|
|
85
|
-
const child = this.#mirror.child;
|
|
86
|
-
|
|
87
|
-
if (child !== undefined) this.fromAgent(child);
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
#thinkingFromAgent(child: ChildInfo): void {
|
|
92
|
-
if (!this.#closed && this.#piHasModel(child) && this.#pi.getThinkingLevel() !== child.thinking) {
|
|
93
|
-
this.#pi.setThinkingLevel(child.thinking);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
#piHasModel(child: ChildInfo): boolean {
|
|
98
|
-
const model = this.#ctx.model;
|
|
99
|
-
|
|
100
|
-
return model?.provider === child.provider && model.id === child.model;
|
|
101
|
-
}
|
|
102
|
-
}
|
package/pi/package.json
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"private": true,
|
|
3
|
-
"description": "The Pi extension of harness: the subagent tool, the /subagent command, the subagents widget and the /harness mirror sessions",
|
|
4
|
-
"type": "module",
|
|
5
|
-
"pi": {
|
|
6
|
-
"extensions": [
|
|
7
|
-
"./index.ts"
|
|
8
|
-
]
|
|
9
|
-
},
|
|
10
|
-
"peerDependencies": {
|
|
11
|
-
"@earendil-works/pi-ai": "*",
|
|
12
|
-
"@earendil-works/pi-coding-agent": "*",
|
|
13
|
-
"@earendil-works/pi-tui": "*"
|
|
14
|
-
}
|
|
15
|
-
}
|
package/pi/program-status.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The state of the agent of a mirror session for the terminal, with the Program Status Protocol (OSC 7501): `working`
|
|
3
|
-
* while it works, `error` when its last request failed, and `idle` in all other cases.
|
|
4
|
-
*/
|
|
5
|
-
import type { ProgramStatus, Terminal } from "@earendil-works/pi-tui";
|
|
6
|
-
import { type Mirror, targetTitle } from "../dist/mirror.js";
|
|
7
|
-
import { childTitle } from "../dist/protocol.js";
|
|
8
|
-
|
|
9
|
-
/** The program name of each report. */
|
|
10
|
-
const APP = "harness";
|
|
11
|
-
|
|
12
|
-
type Mirrored = Pick<Mirror, "target" | "status" | "child" | "view" | "busy">;
|
|
13
|
-
|
|
14
|
-
/** The title of the agent of `mirror`. */
|
|
15
|
-
function title({ target, child }: Pick<Mirrored, "target" | "child">): string {
|
|
16
|
-
return child === undefined ? targetTitle(target) : childTitle(child);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** The first line of the error of the last failed request, or `Error` when there is none. */
|
|
20
|
-
function errorLine({ view }: Pick<Mirrored, "view">): string {
|
|
21
|
-
const failed = view?.entries.findLast((entry) => {
|
|
22
|
-
const message = entry.model?.[0];
|
|
23
|
-
|
|
24
|
-
return entry.kind === "pi.assistant" && message?.role === "assistant" && message.stopReason === "error";
|
|
25
|
-
})?.model?.[0];
|
|
26
|
-
|
|
27
|
-
const text = failed?.role === "assistant" ? failed.errorMessage : undefined;
|
|
28
|
-
|
|
29
|
-
return text?.split(/\r?\n/, 1)[0]?.trim() || "Error";
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** The state of the agent of `mirror`. */
|
|
33
|
-
function programStatus(mirror: Mirrored): ProgramStatus {
|
|
34
|
-
if (mirror.busy) return { state: "working", app: APP, message: title(mirror) };
|
|
35
|
-
|
|
36
|
-
if (mirror.status.kind === "open" && mirror.child?.state === "error") {
|
|
37
|
-
return { state: "error", app: APP, message: errorLine(mirror) };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return { state: "idle", app: APP, message: title(mirror) };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export class ProgramStatusReport {
|
|
44
|
-
readonly #terminal: Pick<Terminal, "setProgramStatus">;
|
|
45
|
-
/** The state and the message of the last report. */
|
|
46
|
-
#last: string | undefined;
|
|
47
|
-
|
|
48
|
-
constructor(terminal: Pick<Terminal, "setProgramStatus">) {
|
|
49
|
-
this.#terminal = terminal;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Reports the state of the agent of `mirror` when its state or its message changed since the last report. */
|
|
53
|
-
update(mirror: Mirrored): void {
|
|
54
|
-
const report = programStatus(mirror);
|
|
55
|
-
const key = `${report.state}\n${report.message}`;
|
|
56
|
-
|
|
57
|
-
if (key === this.#last) return;
|
|
58
|
-
this.#last = key;
|
|
59
|
-
this.#terminal.setProgramStatus(report);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Reports `idle` one more time, when Pi leaves the session of `mirror`. */
|
|
63
|
-
leave(mirror: Pick<Mirrored, "target" | "child">): void {
|
|
64
|
-
this.#terminal.setProgramStatus({ state: "idle", app: APP, message: title(mirror) });
|
|
65
|
-
}
|
|
66
|
-
}
|
package/pi/usage-line.ts
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/** The usage line of a mirror session, above the editor, as Pi's footer shows the usage of its own session. */
|
|
2
|
-
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import * as coding from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import type { ConversationView } from "@earendil-works/pi-durable";
|
|
5
|
-
import { type ChildInfo, childTitle } from "../dist/protocol.js";
|
|
6
|
-
import { lastAnswerUsage, usageTotals } from "../dist/view-state.js";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* The title of the agent, the tokens and the cost of its model requests and tool results, and the context of its last
|
|
10
|
-
* answer that did not abort or fail, against the context window of its model, as Pi's footer shows them for its own
|
|
11
|
-
* session.
|
|
12
|
-
*/
|
|
13
|
-
export function usageLine(
|
|
14
|
-
theme: Theme,
|
|
15
|
-
models: ExtensionContext["modelRegistry"],
|
|
16
|
-
child: ChildInfo,
|
|
17
|
-
view: ConversationView,
|
|
18
|
-
): string {
|
|
19
|
-
const totals = usageTotals(view);
|
|
20
|
-
const last = lastAnswerUsage(view);
|
|
21
|
-
const model = models.find(child.provider, child.model);
|
|
22
|
-
const subscription = model !== undefined && models.isUsingOAuth(model);
|
|
23
|
-
const usage: string[] = [];
|
|
24
|
-
|
|
25
|
-
if (totals.input) usage.push(`↑${formatTokens(totals.input)}`);
|
|
26
|
-
|
|
27
|
-
if (totals.output) usage.push(`↓${formatTokens(totals.output)}`);
|
|
28
|
-
|
|
29
|
-
if (totals.cacheRead) usage.push(`R${formatTokens(totals.cacheRead)}`);
|
|
30
|
-
|
|
31
|
-
if (totals.cacheWrite) usage.push(`W${formatTokens(totals.cacheWrite)}`);
|
|
32
|
-
|
|
33
|
-
if (totals.cost || subscription) usage.push(`$${totals.cost.toFixed(3)}${subscription ? " (sub)" : ""}`);
|
|
34
|
-
const parts = [childTitle(child)];
|
|
35
|
-
|
|
36
|
-
if (usage.length > 0) parts.push(usage.join(" "));
|
|
37
|
-
const contextWindow = model?.contextWindow ?? 0;
|
|
38
|
-
|
|
39
|
-
if (contextWindow > 0) {
|
|
40
|
-
const percent = last === undefined ? 0 : (coding.calculateContextTokens(last) / contextWindow) * 100;
|
|
41
|
-
const context = `${percent.toFixed(1)}%/${formatTokens(contextWindow)}`;
|
|
42
|
-
parts.push(percent > 90 ? theme.fg("error", context) : percent > 70 ? theme.fg("warning", context) : context);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return theme.fg("dim", parts.join(" • "));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** A token count as Pi's footer shows it: `950`, `1.2k`, `45k`, `1.5M`. */
|
|
49
|
-
function formatTokens(count: number): string {
|
|
50
|
-
if (count < 1000) return count.toString();
|
|
51
|
-
|
|
52
|
-
if (count < 10_000) return `${(count / 1000).toFixed(1)}k`;
|
|
53
|
-
|
|
54
|
-
if (count < 1_000_000) return `${Math.round(count / 1000)}k`;
|
|
55
|
-
|
|
56
|
-
if (count < 10_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
|
57
|
-
|
|
58
|
-
return `${Math.round(count / 1_000_000)}M`;
|
|
59
|
-
}
|