@oh-my-pi/pi-coding-agent 14.9.3 → 14.9.7
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/CHANGELOG.md +96 -0
- package/package.json +7 -7
- package/src/async/job-manager.ts +66 -9
- package/src/capability/rule.ts +20 -0
- package/src/cli/setup-cli.ts +14 -161
- package/src/cli/stats-cli.ts +56 -2
- package/src/cli.ts +0 -1
- package/src/config/model-registry.ts +13 -0
- package/src/config/model-resolver.ts +8 -2
- package/src/config/settings-schema.ts +1 -11
- package/src/edit/index.ts +8 -0
- package/src/edit/renderer.ts +6 -1
- package/src/edit/streaming.ts +53 -2
- package/src/eval/eval.lark +30 -10
- package/src/eval/js/context-manager.ts +334 -601
- package/src/eval/js/shared/helpers.ts +237 -0
- package/src/eval/js/shared/indirect-eval.ts +30 -0
- package/src/eval/js/{prelude.txt → shared/prelude.txt} +0 -2
- package/src/eval/js/shared/rewrite-imports.ts +211 -0
- package/src/eval/js/shared/runtime.ts +168 -0
- package/src/eval/js/shared/types.ts +18 -0
- package/src/eval/js/tool-bridge.ts +2 -4
- package/src/eval/js/worker-core.ts +146 -0
- package/src/eval/js/worker-entry.ts +24 -0
- package/src/eval/js/worker-protocol.ts +41 -0
- package/src/eval/parse.ts +218 -49
- package/src/eval/py/display.ts +71 -0
- package/src/eval/py/executor.ts +97 -96
- package/src/eval/py/index.ts +2 -2
- package/src/eval/py/kernel.ts +472 -900
- package/src/eval/py/prelude.py +106 -87
- package/src/eval/py/runner.py +879 -0
- package/src/eval/py/runtime.ts +3 -16
- package/src/eval/py/tool-bridge.ts +137 -0
- package/src/export/html/template.css +12 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +113 -7
- package/src/extensibility/plugins/loader.ts +31 -6
- package/src/extensibility/skills.ts +20 -0
- package/src/internal-urls/agent-protocol.ts +63 -52
- package/src/internal-urls/artifact-protocol.ts +51 -51
- package/src/internal-urls/docs-index.generated.ts +35 -3
- package/src/internal-urls/index.ts +6 -19
- package/src/internal-urls/local-protocol.ts +49 -7
- package/src/internal-urls/mcp-protocol.ts +2 -8
- package/src/internal-urls/memory-protocol.ts +89 -59
- package/src/internal-urls/router.ts +38 -22
- package/src/internal-urls/rule-protocol.ts +2 -20
- package/src/internal-urls/skill-protocol.ts +4 -27
- package/src/main.ts +1 -1
- package/src/mcp/manager.ts +17 -0
- package/src/modes/components/session-observer-overlay.ts +2 -2
- package/src/modes/components/tool-execution.ts +6 -0
- package/src/modes/components/tree-selector.ts +4 -0
- package/src/modes/controllers/command-controller.ts +0 -23
- package/src/modes/controllers/event-controller.ts +23 -2
- package/src/modes/controllers/mcp-command-controller.ts +7 -10
- package/src/modes/interactive-mode.ts +2 -2
- package/src/modes/theme/theme.ts +27 -27
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +14 -9
- package/src/prompts/commands/orchestrate.md +1 -0
- package/src/prompts/system/project-prompt.md +10 -2
- package/src/prompts/system/subagent-system-prompt.md +8 -8
- package/src/prompts/system/system-prompt.md +13 -7
- package/src/prompts/tools/ask.md +0 -1
- package/src/prompts/tools/bash.md +0 -10
- package/src/prompts/tools/eval.md +15 -30
- package/src/prompts/tools/github.md +6 -5
- package/src/prompts/tools/hashline.md +1 -0
- package/src/prompts/tools/job.md +14 -6
- package/src/prompts/tools/task.md +20 -3
- package/src/registry/agent-registry.ts +2 -1
- package/src/sdk.ts +87 -89
- package/src/session/agent-session.ts +58 -21
- package/src/session/artifacts.ts +7 -4
- package/src/session/history-storage.ts +77 -19
- package/src/session/session-manager.ts +30 -1
- package/src/ssh/connection-manager.ts +32 -16
- package/src/ssh/sshfs-mount.ts +10 -7
- package/src/system-prompt.ts +0 -5
- package/src/task/executor.ts +14 -2
- package/src/task/index.ts +19 -5
- package/src/tool-discovery/tool-index.ts +21 -8
- package/src/tools/ast-edit.ts +3 -2
- package/src/tools/ast-grep.ts +3 -2
- package/src/tools/bash.ts +15 -9
- package/src/tools/browser/tab-protocol.ts +4 -0
- package/src/tools/browser/tab-supervisor.ts +98 -7
- package/src/tools/browser/tab-worker.ts +104 -58
- package/src/tools/eval.ts +49 -11
- package/src/tools/fetch.ts +1 -1
- package/src/tools/gh.ts +140 -4
- package/src/tools/index.ts +12 -11
- package/src/tools/job.ts +48 -12
- package/src/tools/read.ts +5 -4
- package/src/tools/search.ts +3 -2
- package/src/tools/todo-write.ts +1 -1
- package/src/web/scrapers/mastodon.ts +1 -1
- package/src/web/scrapers/repology.ts +7 -7
- package/src/web/search/index.ts +6 -4
- package/src/cli/jupyter-cli.ts +0 -106
- package/src/commands/jupyter.ts +0 -32
- package/src/eval/py/cancellation.ts +0 -28
- package/src/eval/py/gateway-coordinator.ts +0 -424
- package/src/internal-urls/jobs-protocol.ts +0 -120
- package/src/prompts/system/now-prompt.md +0 -7
- /package/src/eval/js/{prelude.ts → shared/prelude.ts} +0 -0
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import type { ToolSession } from "../../tools";
|
|
3
3
|
import { ToolError } from "../../tools/tool-errors";
|
|
4
|
+
import type { JsStatusEvent } from "./shared/types";
|
|
4
5
|
|
|
5
|
-
export
|
|
6
|
-
op: string;
|
|
7
|
-
[key: string]: unknown;
|
|
8
|
-
}
|
|
6
|
+
export type { JsStatusEvent } from "./shared/types";
|
|
9
7
|
|
|
10
8
|
interface ToolBridgeOptions {
|
|
11
9
|
session: ToolSession;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { ToolError } from "../../tools/tool-errors";
|
|
2
|
+
import { JsRuntime, type RuntimeHooks } from "./shared/runtime";
|
|
3
|
+
import type { RunErrorPayload, SessionSnapshot, ToolReply, Transport, WorkerInbound } from "./worker-protocol";
|
|
4
|
+
|
|
5
|
+
interface PendingTool {
|
|
6
|
+
resolve(value: unknown): void;
|
|
7
|
+
reject(error: Error): void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ActiveRun {
|
|
11
|
+
runId: string;
|
|
12
|
+
pendingTools: Map<string, PendingTool>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function errorPayload(error: unknown): RunErrorPayload {
|
|
16
|
+
if (error instanceof Error) {
|
|
17
|
+
return {
|
|
18
|
+
name: error.name,
|
|
19
|
+
message: error.message,
|
|
20
|
+
stack: error.stack,
|
|
21
|
+
isAbort: error.name === "AbortError" || error.name === "ToolAbortError",
|
|
22
|
+
isToolError: error.name === "ToolError" || error instanceof ToolError,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return { message: String(error) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function errorFromPayload(payload: RunErrorPayload): Error {
|
|
29
|
+
const ctor = payload.isToolError ? ToolError : Error;
|
|
30
|
+
const error = new ctor(payload.message);
|
|
31
|
+
if (payload.name) error.name = payload.name;
|
|
32
|
+
if (payload.stack) error.stack = payload.stack;
|
|
33
|
+
return error;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class WorkerCore {
|
|
37
|
+
#transport: Transport;
|
|
38
|
+
#runtime: JsRuntime | null = null;
|
|
39
|
+
#queue: Promise<void> = Promise.resolve();
|
|
40
|
+
#active: ActiveRun | null = null;
|
|
41
|
+
#unsubscribe: () => void;
|
|
42
|
+
|
|
43
|
+
constructor(transport: Transport) {
|
|
44
|
+
this.#transport = transport;
|
|
45
|
+
this.#unsubscribe = transport.onMessage(msg => this.#handle(msg));
|
|
46
|
+
transport.send({ type: "ready" });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
#handle(msg: WorkerInbound): void {
|
|
50
|
+
switch (msg.type) {
|
|
51
|
+
case "init":
|
|
52
|
+
this.#ensureRuntime(msg.snapshot);
|
|
53
|
+
return;
|
|
54
|
+
case "run":
|
|
55
|
+
this.#enqueueRun(msg.runId, msg.code, msg.filename, msg.snapshot);
|
|
56
|
+
return;
|
|
57
|
+
case "tool-reply":
|
|
58
|
+
this.#deliverToolReply(msg.id, msg.reply);
|
|
59
|
+
return;
|
|
60
|
+
case "close":
|
|
61
|
+
this.#close();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#ensureRuntime(snapshot: SessionSnapshot): JsRuntime {
|
|
67
|
+
if (this.#runtime) {
|
|
68
|
+
this.#runtime.setCwd(snapshot.cwd);
|
|
69
|
+
return this.#runtime;
|
|
70
|
+
}
|
|
71
|
+
this.#runtime = new JsRuntime({
|
|
72
|
+
initialCwd: snapshot.cwd,
|
|
73
|
+
sessionId: snapshot.sessionId,
|
|
74
|
+
getHooks: () => this.#hooksForCurrentRun(),
|
|
75
|
+
});
|
|
76
|
+
return this.#runtime;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#hooksForCurrentRun(): RuntimeHooks | null {
|
|
80
|
+
const active = this.#active;
|
|
81
|
+
if (!active) return null;
|
|
82
|
+
const runId = active.runId;
|
|
83
|
+
return {
|
|
84
|
+
onText: chunk => this.#transport.send({ type: "text", runId, chunk }),
|
|
85
|
+
onDisplay: output => this.#transport.send({ type: "display", runId, output }),
|
|
86
|
+
callTool: (name, args) => this.#callTool(active, name, args),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#enqueueRun(runId: string, code: string, filename: string, snapshot: SessionSnapshot): void {
|
|
91
|
+
const previous = this.#queue;
|
|
92
|
+
const next = (async () => {
|
|
93
|
+
await previous.catch(() => undefined);
|
|
94
|
+
await this.#runOne(runId, code, filename, snapshot);
|
|
95
|
+
})();
|
|
96
|
+
this.#queue = next.catch(() => undefined);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async #runOne(runId: string, code: string, filename: string, snapshot: SessionSnapshot): Promise<void> {
|
|
100
|
+
const runtime = this.#ensureRuntime(snapshot);
|
|
101
|
+
runtime.setCwd(snapshot.cwd);
|
|
102
|
+
this.#active = { runId, pendingTools: new Map() };
|
|
103
|
+
try {
|
|
104
|
+
const value = await runtime.run(code, filename);
|
|
105
|
+
runtime.displayValue(value);
|
|
106
|
+
this.#transport.send({ type: "result", runId, ok: true });
|
|
107
|
+
} catch (error) {
|
|
108
|
+
this.#transport.send({ type: "result", runId, ok: false, error: errorPayload(error) });
|
|
109
|
+
} finally {
|
|
110
|
+
this.#active = null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async #callTool(active: ActiveRun, name: string, args: unknown): Promise<unknown> {
|
|
115
|
+
const id = `tc-${active.runId}-${crypto.randomUUID()}`;
|
|
116
|
+
const { promise, resolve, reject } = Promise.withResolvers<unknown>();
|
|
117
|
+
active.pendingTools.set(id, { resolve, reject });
|
|
118
|
+
this.#transport.send({ type: "tool-call", id, runId: active.runId, name, args });
|
|
119
|
+
return await promise;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#deliverToolReply(id: string, reply: ToolReply): void {
|
|
123
|
+
const active = this.#active;
|
|
124
|
+
if (!active) return;
|
|
125
|
+
const pending = active.pendingTools.get(id);
|
|
126
|
+
if (!pending) return;
|
|
127
|
+
active.pendingTools.delete(id);
|
|
128
|
+
if (reply.ok) pending.resolve(reply.value);
|
|
129
|
+
else pending.reject(errorFromPayload(reply.error));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
#close(): void {
|
|
133
|
+
const active = this.#active;
|
|
134
|
+
if (active) {
|
|
135
|
+
for (const pending of active.pendingTools.values()) {
|
|
136
|
+
pending.reject(new ToolError("JS worker closed"));
|
|
137
|
+
}
|
|
138
|
+
active.pendingTools.clear();
|
|
139
|
+
}
|
|
140
|
+
this.#active = null;
|
|
141
|
+
this.#runtime = null;
|
|
142
|
+
this.#transport.send({ type: "closed" });
|
|
143
|
+
this.#unsubscribe();
|
|
144
|
+
this.#transport.close();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { parentPort } from "node:worker_threads";
|
|
2
|
+
import { WorkerCore } from "./worker-core";
|
|
3
|
+
import type { Transport, WorkerInbound, WorkerOutbound } from "./worker-protocol";
|
|
4
|
+
|
|
5
|
+
if (!parentPort) throw new Error("js worker-entry: missing parentPort");
|
|
6
|
+
|
|
7
|
+
const port = parentPort;
|
|
8
|
+
const transport: Transport = {
|
|
9
|
+
send: (msg: WorkerOutbound) => port.postMessage(msg),
|
|
10
|
+
onMessage: handler => {
|
|
11
|
+
const wrap = (data: unknown): void => handler(data as WorkerInbound);
|
|
12
|
+
port.on("message", wrap);
|
|
13
|
+
return () => port.off("message", wrap);
|
|
14
|
+
},
|
|
15
|
+
close: () => {
|
|
16
|
+
try {
|
|
17
|
+
port.close();
|
|
18
|
+
} catch {
|
|
19
|
+
// Already closed.
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
new WorkerCore(transport);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { JsDisplayOutput } from "./shared/types";
|
|
2
|
+
|
|
3
|
+
export type { JsDisplayOutput } from "./shared/types";
|
|
4
|
+
|
|
5
|
+
export interface SessionSnapshot {
|
|
6
|
+
cwd: string;
|
|
7
|
+
sessionId: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface RunErrorPayload {
|
|
11
|
+
name?: string;
|
|
12
|
+
message: string;
|
|
13
|
+
stack?: string;
|
|
14
|
+
isAbort?: boolean;
|
|
15
|
+
isToolError?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type ToolReply = { ok: true; value: unknown } | { ok: false; error: RunErrorPayload };
|
|
19
|
+
|
|
20
|
+
export type WorkerInbound =
|
|
21
|
+
| { type: "init"; snapshot: SessionSnapshot }
|
|
22
|
+
| { type: "run"; runId: string; code: string; filename: string; snapshot: SessionSnapshot }
|
|
23
|
+
| { type: "tool-reply"; id: string; reply: ToolReply }
|
|
24
|
+
| { type: "close" };
|
|
25
|
+
|
|
26
|
+
export type WorkerOutbound =
|
|
27
|
+
| { type: "ready" }
|
|
28
|
+
| { type: "init-failed"; error: RunErrorPayload }
|
|
29
|
+
| { type: "text"; runId: string; chunk: string }
|
|
30
|
+
| { type: "display"; runId: string; output: JsDisplayOutput }
|
|
31
|
+
| { type: "tool-call"; id: string; runId: string; name: string; args: unknown }
|
|
32
|
+
| { type: "result"; runId: string; ok: true }
|
|
33
|
+
| { type: "result"; runId: string; ok: false; error: RunErrorPayload }
|
|
34
|
+
| { type: "log"; level: "debug" | "warn" | "error"; msg: string; meta?: Record<string, unknown> }
|
|
35
|
+
| { type: "closed" };
|
|
36
|
+
|
|
37
|
+
export interface Transport {
|
|
38
|
+
send(msg: WorkerOutbound): void;
|
|
39
|
+
onMessage(handler: (msg: WorkerInbound) => void): () => void;
|
|
40
|
+
close(): void;
|
|
41
|
+
}
|
package/src/eval/parse.ts
CHANGED
|
@@ -43,16 +43,19 @@ const LANGUAGE_MAP: Record<string, EvalLanguage> = {
|
|
|
43
43
|
TYPESCRIPT: "js",
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
-
// Markers are case-insensitive, accept ≥2 leading stars (so `**
|
|
47
|
-
// `***
|
|
46
|
+
// Markers are case-insensitive, accept ≥2 leading stars (so `**Cell` and
|
|
47
|
+
// `*** Cell` both work), and tolerate any whitespace (including tabs)
|
|
48
48
|
// between tokens. Models that can't constrain-sample frequently emit minor
|
|
49
|
-
// variations like `**End
|
|
49
|
+
// variations like `**End` or `*** cell py`.
|
|
50
50
|
const STARS = String.raw`\*{2,}`;
|
|
51
|
-
|
|
51
|
+
// Cell header: `*** Cell <attrs...>`. The remainder of the line is captured
|
|
52
|
+
// and tokenized separately so we can handle quoted values.
|
|
53
|
+
const CELL_RE = new RegExp(`^${STARS}\\s*Cell\\b\\s*(.*)$`, "i");
|
|
54
|
+
// `*** End` is a tolerated cell/file terminator. Documented as required at
|
|
55
|
+
// the file level in the lark grammar (the trailing `*** End` quirks GPT-
|
|
56
|
+
// trained models naturally produce), but optional at the parser level.
|
|
52
57
|
const END_RE = new RegExp(`^${STARS}\\s*End\\b.*$`, "i");
|
|
53
|
-
|
|
54
|
-
const TIMEOUT_RE = new RegExp(`^${STARS}\\s*Timeout\\s*:\\s*(\\S+)\\s*$`, "i");
|
|
55
|
-
const RESET_RE = new RegExp(`^${STARS}\\s*Reset\\s*$`, "i");
|
|
58
|
+
// `*** Abort` is the harmony-leak recovery sentinel; see ABORT_WARNING.
|
|
56
59
|
const ABORT_RE = new RegExp(`^${STARS}\\s*Abort\\s*$`, "i");
|
|
57
60
|
|
|
58
61
|
/**
|
|
@@ -62,6 +65,7 @@ const ABORT_RE = new RegExp(`^${STARS}\\s*Abort\\s*$`, "i");
|
|
|
62
65
|
*/
|
|
63
66
|
export const ABORT_WARNING =
|
|
64
67
|
"Tool stream truncated mid-call due to detected output corruption. Earlier cells (if any) executed normally; their state persists. Re-issue the aborted cell.";
|
|
68
|
+
|
|
65
69
|
const DURATION_RE = /^(\d+)(ms|s|m)?$/i;
|
|
66
70
|
|
|
67
71
|
function resolveLang(token: string | undefined): EvalLanguage | undefined {
|
|
@@ -88,7 +92,7 @@ const FENCE_OPEN_RE = /^```\s*([A-Za-z]\w*)?\s*$/;
|
|
|
88
92
|
const FENCE_CLOSE_RE = /^```\s*$/;
|
|
89
93
|
|
|
90
94
|
/**
|
|
91
|
-
* Last-resort fallback when the input has no recognizable `***
|
|
95
|
+
* Last-resort fallback when the input has no recognizable `*** Cell` header.
|
|
92
96
|
* Models that can't constrain-sample sometimes pass bare code or wrap it in
|
|
93
97
|
* a markdown fence (```py / ```python / bare ```). Treat the whole input as
|
|
94
98
|
* a single implicit cell, sniffing the language from the body.
|
|
@@ -122,6 +126,187 @@ function parseImplicitCell(lines: string[]): ParsedEvalCell {
|
|
|
122
126
|
};
|
|
123
127
|
}
|
|
124
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Tokenize a `*** Cell` header's attribute list while preserving quoted
|
|
131
|
+
* segments (`id:"some title"`, `py:"hi"`, single quotes too) as single
|
|
132
|
+
* tokens. Outer whitespace separates tokens; the quote characters
|
|
133
|
+
* themselves are kept verbatim so attribute parsing can strip them later.
|
|
134
|
+
*/
|
|
135
|
+
function tokenizeCellAttrs(input: string): string[] {
|
|
136
|
+
const tokens: string[] = [];
|
|
137
|
+
let i = 0;
|
|
138
|
+
while (i < input.length) {
|
|
139
|
+
while (i < input.length && /\s/.test(input[i])) i++;
|
|
140
|
+
if (i >= input.length) break;
|
|
141
|
+
let token = "";
|
|
142
|
+
while (i < input.length && !/\s/.test(input[i])) {
|
|
143
|
+
const ch = input[i];
|
|
144
|
+
if (ch === '"' || ch === "'") {
|
|
145
|
+
token += ch;
|
|
146
|
+
i++;
|
|
147
|
+
while (i < input.length && input[i] !== ch) {
|
|
148
|
+
token += input[i];
|
|
149
|
+
i++;
|
|
150
|
+
}
|
|
151
|
+
if (i < input.length) {
|
|
152
|
+
token += input[i];
|
|
153
|
+
i++;
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
token += ch;
|
|
157
|
+
i++;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
tokens.push(token);
|
|
161
|
+
}
|
|
162
|
+
return tokens;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
interface CellHeader {
|
|
166
|
+
language: EvalLanguage | undefined;
|
|
167
|
+
languageOrigin: EvalLanguageOrigin;
|
|
168
|
+
title: string | undefined;
|
|
169
|
+
timeoutMs: number | undefined;
|
|
170
|
+
reset: boolean;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Map an attribute key (from `key:value` or bare `key`) to one of the three
|
|
175
|
+
* canonical roles. Canonical keys: `id`, `t`, `rst`. Fallback aliases —
|
|
176
|
+
* accepted but not advertised in the prompt — cover common synonyms LLMs
|
|
177
|
+
* reach for instead of the short canonical.
|
|
178
|
+
*/
|
|
179
|
+
const ID_KEYS = new Set(["id", "title", "name", "cell", "file", "label"]);
|
|
180
|
+
const T_KEYS = new Set(["t", "timeout", "duration", "time"]);
|
|
181
|
+
const RST_KEYS = new Set(["rst", "reset"]);
|
|
182
|
+
|
|
183
|
+
function classifyAttrKey(key: string): "id" | "t" | "rst" | null {
|
|
184
|
+
if (ID_KEYS.has(key)) return "id";
|
|
185
|
+
if (T_KEYS.has(key)) return "t";
|
|
186
|
+
if (RST_KEYS.has(key)) return "rst";
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// `key:value` form. `value` may be `"..."`, `'...'`, or a bare run.
|
|
191
|
+
const ATTR_TOKEN_RE = /^([a-zA-Z][\w-]*)(?::(?:"([^"]*)"|'([^']*)'|(.*)))?$/;
|
|
192
|
+
// Bare positional duration (lenient — `t:` is canonical).
|
|
193
|
+
const DURATION_TOKEN_RE = /^\d+(?:ms|s|m)?$/;
|
|
194
|
+
|
|
195
|
+
function parseBooleanFlag(value: string): boolean | undefined {
|
|
196
|
+
const v = value.trim().toLowerCase();
|
|
197
|
+
if (v === "true" || v === "1" || v === "yes" || v === "on") return true;
|
|
198
|
+
if (v === "false" || v === "0" || v === "no" || v === "off") return false;
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Decode a `*** Cell` header's attribute list into language, title,
|
|
204
|
+
* timeout, and reset flag.
|
|
205
|
+
*
|
|
206
|
+
* Token forms (all optional, any order):
|
|
207
|
+
* - `py` / `js` / `ts` bare language
|
|
208
|
+
* - `py:"..."` / `js:"..."` / `ts:"..."` language + title shorthand
|
|
209
|
+
* - `id:"..."` cell title (canonical)
|
|
210
|
+
* - `t:<duration>` per-cell timeout (canonical)
|
|
211
|
+
* - `<duration>` (e.g. `30s`) bare positional duration
|
|
212
|
+
* - `rst` reset flag (canonical)
|
|
213
|
+
* - `rst:true|false|1|0|yes|no|on|off` reset flag with explicit value
|
|
214
|
+
*
|
|
215
|
+
* Fallback aliases (accepted but not advertised in the prompt):
|
|
216
|
+
* - id: title, name, cell, file, label
|
|
217
|
+
* - t: timeout, duration, time
|
|
218
|
+
* - rst: reset
|
|
219
|
+
*
|
|
220
|
+
* Quotes may be `"` or `'`. Truly unknown keys are silently dropped. First
|
|
221
|
+
* occurrence wins when a key is repeated (canonical or alias). Anything
|
|
222
|
+
* that doesn't classify accumulates as a positional title fragment joined
|
|
223
|
+
* by spaces.
|
|
224
|
+
*/
|
|
225
|
+
function parseCellHeader(rest: string, lineNumber: number): CellHeader {
|
|
226
|
+
const tokens = tokenizeCellAttrs(rest);
|
|
227
|
+
let language: EvalLanguage | undefined;
|
|
228
|
+
let titleAttr: string | undefined;
|
|
229
|
+
let positionalDurationMs: number | undefined;
|
|
230
|
+
let tAttr: string | undefined;
|
|
231
|
+
let rstAttr: string | undefined;
|
|
232
|
+
let bareReset = false;
|
|
233
|
+
const titleParts: string[] = [];
|
|
234
|
+
|
|
235
|
+
for (const token of tokens) {
|
|
236
|
+
// Bare reset flag (canonical or alias).
|
|
237
|
+
if (RST_KEYS.has(token.toLowerCase())) {
|
|
238
|
+
bareReset = true;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const attrMatch = ATTR_TOKEN_RE.exec(token);
|
|
243
|
+
if (attrMatch && token.includes(":")) {
|
|
244
|
+
const key = attrMatch[1].toLowerCase();
|
|
245
|
+
const value = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
|
|
246
|
+
|
|
247
|
+
// Language-with-title shorthand: `py:"foo"`, `js:'bar'`, etc.
|
|
248
|
+
const langCandidate = resolveLang(key);
|
|
249
|
+
if (langCandidate) {
|
|
250
|
+
if (language === undefined) language = langCandidate;
|
|
251
|
+
if (titleAttr === undefined && value !== "") titleAttr = value;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const role = classifyAttrKey(key);
|
|
256
|
+
if (role === "id" && titleAttr === undefined) titleAttr = value;
|
|
257
|
+
else if (role === "t" && tAttr === undefined) tAttr = value;
|
|
258
|
+
else if (role === "rst" && rstAttr === undefined) rstAttr = value;
|
|
259
|
+
// unknown / repeated keys silently dropped
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Bare language token (no colon).
|
|
264
|
+
const lang = resolveLang(token);
|
|
265
|
+
if (lang && language === undefined) {
|
|
266
|
+
language = lang;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Bare positional duration (lenient — `t:` is canonical).
|
|
271
|
+
if (positionalDurationMs === undefined && DURATION_TOKEN_RE.test(token)) {
|
|
272
|
+
positionalDurationMs = parseDurationMs(token, lineNumber);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
titleParts.push(token);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const explicitTitle = (titleAttr ?? "").trim();
|
|
280
|
+
const positionalTitle = titleParts.join(" ").trim();
|
|
281
|
+
const title = explicitTitle.length > 0 ? explicitTitle : positionalTitle.length > 0 ? positionalTitle : undefined;
|
|
282
|
+
|
|
283
|
+
let timeoutMs: number | undefined;
|
|
284
|
+
if (tAttr !== undefined) {
|
|
285
|
+
timeoutMs = parseDurationMs(tAttr, lineNumber);
|
|
286
|
+
} else if (positionalDurationMs !== undefined) {
|
|
287
|
+
timeoutMs = positionalDurationMs;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let reset = false;
|
|
291
|
+
if (rstAttr !== undefined) {
|
|
292
|
+
const parsed = parseBooleanFlag(rstAttr);
|
|
293
|
+
if (parsed === undefined) {
|
|
294
|
+
throw new Error(`Eval line ${lineNumber}: invalid rst value \`${rstAttr}\`; use true or false.`);
|
|
295
|
+
}
|
|
296
|
+
reset = parsed;
|
|
297
|
+
} else if (bareReset) {
|
|
298
|
+
reset = true;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
language,
|
|
303
|
+
languageOrigin: language ? "header" : "default",
|
|
304
|
+
title,
|
|
305
|
+
timeoutMs,
|
|
306
|
+
reset,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
125
310
|
export function parseEvalInput(input: string): ParsedEvalInput {
|
|
126
311
|
const normalized = input.replace(/\r\n?/g, "\n");
|
|
127
312
|
const lines = normalized.split("\n");
|
|
@@ -134,10 +319,10 @@ export function parseEvalInput(input: string): ParsedEvalInput {
|
|
|
134
319
|
// Skip leading blank lines.
|
|
135
320
|
while (i < lines.length && lines[i].trim() === "") i++;
|
|
136
321
|
|
|
137
|
-
// Lenient fallback: if the input has no recognizable
|
|
322
|
+
// Lenient fallback: if the input has no recognizable cell header, treat
|
|
138
323
|
// the entire input as one implicit cell — unless that content contains
|
|
139
324
|
// `*** Abort`, in which case the body is incomplete/unsafe and we drop it.
|
|
140
|
-
if (i < lines.length && !
|
|
325
|
+
if (i < lines.length && !CELL_RE.test(lines[i])) {
|
|
141
326
|
const tail = lines.slice(i);
|
|
142
327
|
if (tail.some(line => ABORT_RE.test(line))) {
|
|
143
328
|
return { cells, aborted: true };
|
|
@@ -148,42 +333,26 @@ export function parseEvalInput(input: string): ParsedEvalInput {
|
|
|
148
333
|
}
|
|
149
334
|
|
|
150
335
|
while (i < lines.length) {
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const line = lines[i];
|
|
162
|
-
const lineNumber = i + 1;
|
|
163
|
-
const titleMatch = TITLE_RE.exec(line);
|
|
164
|
-
if (titleMatch) {
|
|
165
|
-
if (title === undefined) title = titleMatch[1];
|
|
166
|
-
i++;
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
const timeoutMatch = TIMEOUT_RE.exec(line);
|
|
170
|
-
if (timeoutMatch) {
|
|
171
|
-
if (timeoutMs === undefined) timeoutMs = parseDurationMs(timeoutMatch[1], lineNumber);
|
|
172
|
-
i++;
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
if (RESET_RE.test(line)) {
|
|
176
|
-
reset = true;
|
|
177
|
-
i++;
|
|
178
|
-
continue;
|
|
336
|
+
const headerLine = lines[i];
|
|
337
|
+
const cellMatch = CELL_RE.exec(headerLine);
|
|
338
|
+
if (!cellMatch) {
|
|
339
|
+
// Stray content between/after cells (blank lines were already
|
|
340
|
+
// consumed). `*** Abort` here terminates parsing; `*** End` is
|
|
341
|
+
// the optional file-level terminator (silently consumed). Anything
|
|
342
|
+
// else — typically a harmony-leak fragment — is skipped.
|
|
343
|
+
if (ABORT_RE.test(headerLine)) {
|
|
344
|
+
aborted = true;
|
|
345
|
+
break;
|
|
179
346
|
}
|
|
180
|
-
|
|
347
|
+
i++;
|
|
348
|
+
continue;
|
|
181
349
|
}
|
|
350
|
+
const header = parseCellHeader(cellMatch[1] ?? "", i + 1);
|
|
351
|
+
i++;
|
|
182
352
|
|
|
183
|
-
// Collect cell body. Close on `*** End`
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
// in-progress cell entirely: its body is partial and unsafe to run.
|
|
353
|
+
// Collect cell body. Close on `*** End` (any form), the next
|
|
354
|
+
// `*** Cell` header, or `*** Abort` (which drops the in-progress
|
|
355
|
+
// cell as its body is partial and unsafe to run).
|
|
187
356
|
const codeLines: string[] = [];
|
|
188
357
|
let cellAborted = false;
|
|
189
358
|
while (i < lines.length) {
|
|
@@ -198,7 +367,7 @@ export function parseEvalInput(input: string): ParsedEvalInput {
|
|
|
198
367
|
i++;
|
|
199
368
|
break;
|
|
200
369
|
}
|
|
201
|
-
if (
|
|
370
|
+
if (CELL_RE.test(line)) break;
|
|
202
371
|
codeLines.push(line);
|
|
203
372
|
i++;
|
|
204
373
|
}
|
|
@@ -212,17 +381,17 @@ export function parseEvalInput(input: string): ParsedEvalInput {
|
|
|
212
381
|
}
|
|
213
382
|
const code = codeLines.join("\n");
|
|
214
383
|
|
|
215
|
-
const language =
|
|
216
|
-
const languageOrigin: EvalLanguageOrigin =
|
|
384
|
+
const language = header.language ?? sniffEvalLanguage(code) ?? DEFAULT_LANGUAGE;
|
|
385
|
+
const languageOrigin: EvalLanguageOrigin = header.language ? "header" : "default";
|
|
217
386
|
|
|
218
387
|
cells.push({
|
|
219
388
|
index: cells.length,
|
|
220
|
-
title,
|
|
389
|
+
title: header.title,
|
|
221
390
|
code,
|
|
222
391
|
language,
|
|
223
392
|
languageOrigin,
|
|
224
|
-
timeoutMs: timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
225
|
-
reset,
|
|
393
|
+
timeoutMs: header.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
394
|
+
reset: header.reset,
|
|
226
395
|
});
|
|
227
396
|
|
|
228
397
|
// Skip blank separator lines between cells; an `*** Abort` here
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display bundle rendering shared between the Python runner output and the
|
|
3
|
+
* legacy Jupyter MIME conventions. Pure function, no kernel coupling.
|
|
4
|
+
*/
|
|
5
|
+
import { htmlToBasicMarkdown } from "../../web/scrapers/types";
|
|
6
|
+
|
|
7
|
+
/** Status event emitted by prelude helpers for TUI rendering. */
|
|
8
|
+
export interface PythonStatusEvent {
|
|
9
|
+
/** Operation name (e.g., "find", "read", "write") */
|
|
10
|
+
op: string;
|
|
11
|
+
/** Additional data fields (count, path, pattern, etc.) */
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type KernelDisplayOutput =
|
|
16
|
+
| { type: "json"; data: unknown }
|
|
17
|
+
| { type: "image"; data: string; mimeType: string }
|
|
18
|
+
| { type: "markdown" }
|
|
19
|
+
| { type: "status"; event: PythonStatusEvent };
|
|
20
|
+
|
|
21
|
+
function normalizeDisplayText(text: string): string {
|
|
22
|
+
return text.endsWith("\n") ? text : `${text}\n`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Render a MIME bundle into text + structured outputs. */
|
|
26
|
+
export async function renderKernelDisplay(content: Record<string, unknown>): Promise<{
|
|
27
|
+
text: string;
|
|
28
|
+
outputs: KernelDisplayOutput[];
|
|
29
|
+
}> {
|
|
30
|
+
// Accept both raw bundles ({"text/plain": ...}) and Jupyter-style
|
|
31
|
+
// content envelopes ({ data: {...} }) so callers don't need to unwrap.
|
|
32
|
+
const data =
|
|
33
|
+
(content.data as Record<string, unknown> | undefined) ?? (content as Record<string, unknown> | undefined);
|
|
34
|
+
if (!data) return { text: "", outputs: [] };
|
|
35
|
+
|
|
36
|
+
const outputs: KernelDisplayOutput[] = [];
|
|
37
|
+
|
|
38
|
+
// Status events bypass the text path entirely — they exist only for TUI hooks.
|
|
39
|
+
if (data["application/x-omp-status"] !== undefined) {
|
|
40
|
+
const statusData = data["application/x-omp-status"];
|
|
41
|
+
if (statusData && typeof statusData === "object" && "op" in statusData) {
|
|
42
|
+
outputs.push({ type: "status", event: statusData as PythonStatusEvent });
|
|
43
|
+
}
|
|
44
|
+
return { text: "", outputs };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (typeof data["image/png"] === "string") {
|
|
48
|
+
outputs.push({ type: "image", data: data["image/png"] as string, mimeType: "image/png" });
|
|
49
|
+
}
|
|
50
|
+
if (typeof data["image/jpeg"] === "string") {
|
|
51
|
+
outputs.push({ type: "image", data: data["image/jpeg"] as string, mimeType: "image/jpeg" });
|
|
52
|
+
}
|
|
53
|
+
if (data["application/json"] !== undefined) {
|
|
54
|
+
outputs.push({ type: "json", data: data["application/json"] });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// text/markdown takes precedence over text/plain (Markdown objects expose both
|
|
58
|
+
// where text/plain is just the repr).
|
|
59
|
+
if (typeof data["text/markdown"] === "string") {
|
|
60
|
+
outputs.push({ type: "markdown" });
|
|
61
|
+
return { text: normalizeDisplayText(String(data["text/markdown"])), outputs };
|
|
62
|
+
}
|
|
63
|
+
if (typeof data["text/plain"] === "string") {
|
|
64
|
+
return { text: normalizeDisplayText(String(data["text/plain"])), outputs };
|
|
65
|
+
}
|
|
66
|
+
if (data["text/html"] !== undefined) {
|
|
67
|
+
const markdown = (await htmlToBasicMarkdown(String(data["text/html"]))) || "";
|
|
68
|
+
return { text: markdown ? normalizeDisplayText(markdown) : "", outputs };
|
|
69
|
+
}
|
|
70
|
+
return { text: "", outputs };
|
|
71
|
+
}
|