@oh-my-pi/pi-coding-agent 15.8.2 → 15.9.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/CHANGELOG.md +82 -29
- package/dist/types/capability/skill.d.ts +7 -0
- package/dist/types/config/settings-schema.d.ts +36 -22
- package/dist/types/debug/protocol-probe.d.ts +38 -0
- package/dist/types/debug/terminal-info.d.ts +34 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
- package/dist/types/extensibility/extensions/types.d.ts +11 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/mcp/transports/stdio.d.ts +16 -7
- package/dist/types/modes/components/assistant-message.d.ts +3 -2
- package/dist/types/modes/components/hook-selector.d.ts +11 -0
- package/dist/types/modes/components/todo-reminder.d.ts +1 -1
- package/dist/types/modes/components/transcript-container.d.ts +36 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +20 -1
- package/dist/types/plan-mode/plan-handoff.d.ts +20 -0
- package/dist/types/plan-mode/plan-protection.d.ts +12 -0
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/indexed-session-storage.d.ts +59 -0
- package/dist/types/session/redis-session-storage.d.ts +12 -85
- package/dist/types/session/session-manager.d.ts +20 -0
- package/dist/types/session/session-storage.d.ts +5 -7
- package/dist/types/session/sql-session-storage.d.ts +16 -85
- package/dist/types/task/executor.d.ts +9 -0
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/telemetry-export.d.ts +19 -0
- package/dist/types/tiny/compiled-runtime.d.ts +35 -0
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/index.d.ts +4 -2
- package/dist/types/tools/path-utils.d.ts +1 -1
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/tools/{todo-write.d.ts → todo.d.ts} +18 -18
- package/dist/types/utils/jj.d.ts +1 -1
- package/dist/types/utils/session-color.d.ts +7 -2
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/provider.d.ts +2 -2
- package/dist/types/web/search/types.d.ts +65 -1
- package/package.json +15 -9
- package/scripts/build-binary.ts +12 -0
- package/src/capability/skill.ts +7 -0
- package/src/cli/args.ts +1 -1
- package/src/config/settings-schema.ts +22 -55
- package/src/debug/index.ts +67 -1
- package/src/debug/protocol-probe.ts +267 -0
- package/src/debug/terminal-info.ts +127 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +3 -3
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/types.ts +11 -0
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/skills.ts +3 -3
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/main.ts +12 -0
- package/src/mcp/transports/stdio.ts +37 -12
- package/src/modes/acp/acp-event-mapper.ts +7 -7
- package/src/modes/components/assistant-message.ts +3 -2
- package/src/modes/components/hook-selector.ts +149 -14
- package/src/modes/components/session-selector.ts +36 -5
- package/src/modes/components/status-line/segments.ts +2 -1
- package/src/modes/components/status-line.ts +1 -1
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/components/todo-reminder.ts +1 -1
- package/src/modes/components/tool-execution.ts +9 -4
- package/src/modes/components/transcript-container.ts +91 -0
- package/src/modes/controllers/event-controller.ts +12 -7
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/todo-command-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +18 -6
- package/src/modes/print-mode.ts +5 -0
- package/src/modes/rpc/rpc-types.ts +1 -1
- package/src/modes/theme/theme.ts +48 -8
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/plan-mode/plan-handoff.ts +37 -0
- package/src/plan-mode/plan-protection.ts +31 -0
- package/src/priority.json +4 -0
- package/src/prompts/goals/goal-continuation.md +1 -1
- package/src/prompts/system/eager-todo.md +3 -3
- package/src/prompts/system/orchestrate-notice.md +5 -5
- package/src/prompts/system/plan-mode-approved.md +14 -17
- package/src/prompts/system/plan-mode-reference.md +3 -6
- package/src/prompts/system/subagent-system-prompt.md +11 -0
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/browser.md +5 -2
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/task.md +3 -1
- package/src/prompts/tools/{todo-write.md → todo.md} +8 -0
- package/src/sdk.ts +1 -0
- package/src/session/agent-session.ts +29 -16
- package/src/session/indexed-session-storage.ts +430 -0
- package/src/session/redis-session-storage.ts +66 -377
- package/src/session/session-manager.ts +102 -22
- package/src/session/session-storage.ts +148 -68
- package/src/session/sql-session-storage.ts +131 -382
- package/src/slash-commands/helpers/todo.ts +2 -2
- package/src/task/executor.ts +9 -1
- package/src/task/index.ts +51 -1
- package/src/telemetry-export.ts +126 -0
- package/src/tiny/compiled-runtime.ts +179 -0
- package/src/tiny/worker.ts +24 -2
- package/src/tools/ask.ts +133 -87
- package/src/tools/fetch.ts +17 -4
- package/src/tools/find.ts +2 -2
- package/src/tools/index.ts +6 -4
- package/src/tools/path-utils.ts +16 -7
- package/src/tools/renderers.ts +2 -2
- package/src/tools/search.ts +6 -3
- package/src/tools/{todo-write.ts → todo.ts} +32 -35
- package/src/utils/jj.ts +28 -5
- package/src/utils/session-color.ts +39 -14
- package/src/web/search/index.ts +1 -1
- package/src/web/search/provider.ts +18 -34
- package/src/web/search/types.ts +73 -32
package/src/task/index.ts
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
// Import review tools for side effects (registers subagent tool handlers)
|
|
42
42
|
import "../tools/review";
|
|
43
43
|
import type { LocalProtocolOptions } from "../internal-urls";
|
|
44
|
+
import { loadOverallPlanReference } from "../plan-mode/plan-handoff";
|
|
44
45
|
import { generateCommitMessage } from "../utils/commit-message-generator";
|
|
45
46
|
import * as git from "../utils/git";
|
|
46
47
|
import { discoverAgents, getAgent } from "./discovery";
|
|
@@ -131,6 +132,37 @@ export {
|
|
|
131
132
|
taskSchema,
|
|
132
133
|
} from "./types";
|
|
133
134
|
|
|
135
|
+
// Built-in tools whose approval tier is "read" (see tool classes' `approval`).
|
|
136
|
+
// An agent is read-only iff its declared tools are a non-empty subset of this set.
|
|
137
|
+
// Fail-safe: any unknown tool makes the agent not read-only.
|
|
138
|
+
export const READ_ONLY_TOOL_NAMES: ReadonlySet<string> = new Set([
|
|
139
|
+
"read",
|
|
140
|
+
"search",
|
|
141
|
+
"find",
|
|
142
|
+
"web_search",
|
|
143
|
+
"ast_grep",
|
|
144
|
+
"yield",
|
|
145
|
+
"irc",
|
|
146
|
+
"ask",
|
|
147
|
+
"job",
|
|
148
|
+
"todo",
|
|
149
|
+
"recall",
|
|
150
|
+
"reflect",
|
|
151
|
+
"retain",
|
|
152
|
+
"memory_edit",
|
|
153
|
+
"render_mermaid",
|
|
154
|
+
"inspect_image",
|
|
155
|
+
"checkpoint",
|
|
156
|
+
"rewind",
|
|
157
|
+
"resolve",
|
|
158
|
+
"report_finding",
|
|
159
|
+
"search_tool_bm25",
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
export function isReadOnlyAgent(agent: AgentDefinition): boolean {
|
|
163
|
+
return !!agent.tools?.length && agent.tools.every(tool => READ_ONLY_TOOL_NAMES.has(tool));
|
|
164
|
+
}
|
|
165
|
+
|
|
134
166
|
/**
|
|
135
167
|
* Render the tool description from a cached agent list and current settings.
|
|
136
168
|
*/
|
|
@@ -157,9 +189,14 @@ function renderDescription(
|
|
|
157
189
|
);
|
|
158
190
|
filteredAgents = filteredAgents.filter(a => allowed.has(a.name));
|
|
159
191
|
}
|
|
192
|
+
const renderedAgents = filteredAgents.map(agent => ({
|
|
193
|
+
name: agent.name,
|
|
194
|
+
description: agent.description,
|
|
195
|
+
readOnly: isReadOnlyAgent(agent),
|
|
196
|
+
}));
|
|
160
197
|
const { contextEnabled, customSchemaEnabled } = getTaskSimpleModeCapabilities(simpleMode);
|
|
161
198
|
return prompt.render(taskDescriptionTemplate, {
|
|
162
|
-
agents:
|
|
199
|
+
agents: renderedAgents,
|
|
163
200
|
spawningDisabled,
|
|
164
201
|
MAX_CONCURRENCY: maxConcurrency,
|
|
165
202
|
isolationEnabled,
|
|
@@ -772,6 +809,17 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
772
809
|
// across the whole tree and outputs land flat in the parent's dir.
|
|
773
810
|
const parentArtifactManager = this.session.getArtifactManager?.() ?? undefined;
|
|
774
811
|
|
|
812
|
+
// When the session is executing an approved plan, hand the overall plan to
|
|
813
|
+
// every subagent so they share the main agent's plan context. Skipped in
|
|
814
|
+
// plan mode (read-only exploration uses planModeSubagentPrompt instead) and
|
|
815
|
+
// when no plan file exists at the session's reference path.
|
|
816
|
+
const planReference = planModeState?.enabled
|
|
817
|
+
? undefined
|
|
818
|
+
: await loadOverallPlanReference(
|
|
819
|
+
this.session.getPlanReferencePath?.() ?? "local://PLAN.md",
|
|
820
|
+
localProtocolOptions,
|
|
821
|
+
);
|
|
822
|
+
|
|
775
823
|
// Initialize progress tracking
|
|
776
824
|
const progressMap = new Map<number, AgentProgress>();
|
|
777
825
|
|
|
@@ -898,6 +946,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
898
946
|
task: renderSubagentUserPrompt(task.assignment, simpleMode),
|
|
899
947
|
assignment: task.assignment.trim(),
|
|
900
948
|
context: sharedContext,
|
|
949
|
+
planReference,
|
|
901
950
|
description: task.description,
|
|
902
951
|
index,
|
|
903
952
|
id: task.id,
|
|
@@ -955,6 +1004,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
955
1004
|
task: renderSubagentUserPrompt(task.assignment, simpleMode),
|
|
956
1005
|
assignment: task.assignment.trim(),
|
|
957
1006
|
context: sharedContext,
|
|
1007
|
+
planReference,
|
|
958
1008
|
description: task.description,
|
|
959
1009
|
index,
|
|
960
1010
|
id: task.id,
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OTLP trace export bootstrap.
|
|
3
|
+
*
|
|
4
|
+
* oh-my-pi's agent core (`@oh-my-pi/pi-agent-core`) emits OpenTelemetry GenAI
|
|
5
|
+
* spans through the global `@opentelemetry/api` tracer, but only when a
|
|
6
|
+
* TracerProvider is registered in the process — otherwise the API returns a
|
|
7
|
+
* no-op tracer and the spans are silently dropped. The shipped CLI never
|
|
8
|
+
* registered one, so headless / embedded hosts (e.g. an ACP harness that
|
|
9
|
+
* spawns `omp` as a child process) had no way to collect omp's internal traces.
|
|
10
|
+
*
|
|
11
|
+
* This module registers a NodeTracerProvider with an OTLP/proto exporter when
|
|
12
|
+
* the standard `OTEL_EXPORTER_OTLP_ENDPOINT` (or `..._TRACES_ENDPOINT`) env var
|
|
13
|
+
* is set, following the zero-code OTEL env contract: the exporter reads its
|
|
14
|
+
* endpoint, headers, and timeout from `OTEL_EXPORTER_OTLP_*` itself. The
|
|
15
|
+
* consuming process configures the destination entirely through env; omp stays
|
|
16
|
+
* provider-agnostic and ships no vendor coupling. Only the `http/protobuf`
|
|
17
|
+
* transport is supported — an `OTEL_EXPORTER_OTLP*_PROTOCOL` of `grpc` or
|
|
18
|
+
* `http/json` declines rather than misrouting spans.
|
|
19
|
+
*
|
|
20
|
+
* The OTLP/proto exporter on the 2.x line is used deliberately: the 1.x line
|
|
21
|
+
* deadlocks under Bun — its `req.on('close')` handler fires a spurious failure
|
|
22
|
+
* after the success path. `exporter-trace-otlp-proto@0.218` paired with
|
|
23
|
+
* `sdk-trace-base@2.7` exports cleanly on Bun.
|
|
24
|
+
*/
|
|
25
|
+
import { logger, postmortem } from "@oh-my-pi/pi-utils";
|
|
26
|
+
import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
|
|
27
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
|
28
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
29
|
+
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
30
|
+
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Periodic flush interval. A long-lived `omp` process (the ACP server is
|
|
34
|
+
* spawned once and reused across many turns) would otherwise hold finished
|
|
35
|
+
* spans until the batch window elapses or the process exits.
|
|
36
|
+
*/
|
|
37
|
+
const FLUSH_INTERVAL_MS = 30_000;
|
|
38
|
+
|
|
39
|
+
let provider: NodeTracerProvider | undefined;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether {@link initTelemetryExport} registered a real provider. The CLI uses
|
|
43
|
+
* this to decide whether to switch on the agent loop's telemetry config — there
|
|
44
|
+
* is no point emitting spans into a no-op tracer.
|
|
45
|
+
*/
|
|
46
|
+
export function isTelemetryExportEnabled(): boolean {
|
|
47
|
+
return provider !== undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Register the global TracerProvider + OTLP exporter when an OTLP endpoint is
|
|
52
|
+
* configured via env. Idempotent, and a no-op when no endpoint is set (or when
|
|
53
|
+
* the OTEL kill-switches are engaged), so it is safe to call unconditionally at
|
|
54
|
+
* startup.
|
|
55
|
+
*/
|
|
56
|
+
export function initTelemetryExport(): void {
|
|
57
|
+
if (provider) return;
|
|
58
|
+
// The OTEL env contract parses booleans and enum lists case-insensitively, so
|
|
59
|
+
// OTEL_SDK_DISABLED=TRUE and OTEL_TRACES_EXPORTER=None must also disable export.
|
|
60
|
+
if (process.env.OTEL_SDK_DISABLED?.trim().toLowerCase() === "true") return;
|
|
61
|
+
if (tracesExporterDisabled(process.env.OTEL_TRACES_EXPORTER)) return;
|
|
62
|
+
|
|
63
|
+
const endpoint = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
64
|
+
if (!endpoint) return;
|
|
65
|
+
|
|
66
|
+
// We only ship the http/protobuf transport (the line validated on Bun). The
|
|
67
|
+
// OTEL contract lets OTEL_EXPORTER_OTLP*_PROTOCOL select grpc / http/json;
|
|
68
|
+
// rather than silently send protobuf-over-HTTP to a grpc :4317 port and lose
|
|
69
|
+
// every span, decline when an unsupported protocol is requested.
|
|
70
|
+
const protocol = (process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL ?? process.env.OTEL_EXPORTER_OTLP_PROTOCOL)
|
|
71
|
+
?.trim()
|
|
72
|
+
.toLowerCase();
|
|
73
|
+
if (protocol && protocol !== "http/protobuf") {
|
|
74
|
+
logger.warn(
|
|
75
|
+
`OTEL trace export disabled: OTEL_EXPORTER_OTLP_PROTOCOL=${protocol} is unsupported (only http/protobuf)`,
|
|
76
|
+
);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// The exporter reads endpoint/headers/timeout from OTEL_EXPORTER_OTLP_* itself,
|
|
81
|
+
// so there is nothing to thread through here.
|
|
82
|
+
const exporter = new OTLPTraceExporter();
|
|
83
|
+
const tracerProvider = new NodeTracerProvider({
|
|
84
|
+
resource: resourceFromAttributes({
|
|
85
|
+
"service.name": process.env.OTEL_SERVICE_NAME ?? "oh-my-pi",
|
|
86
|
+
}),
|
|
87
|
+
spanProcessors: [new BatchSpanProcessor(exporter)],
|
|
88
|
+
});
|
|
89
|
+
// register() installs the global tracer provider and the W3C trace-context +
|
|
90
|
+
// baggage propagators; the explicit AsyncLocalStorage context manager keeps
|
|
91
|
+
// parent/child span linkage working under Bun.
|
|
92
|
+
tracerProvider.register({ contextManager: new AsyncLocalStorageContextManager().enable() });
|
|
93
|
+
provider = tracerProvider;
|
|
94
|
+
|
|
95
|
+
const flushTimer = setInterval(() => {
|
|
96
|
+
provider?.forceFlush().catch(() => {});
|
|
97
|
+
}, FLUSH_INTERVAL_MS);
|
|
98
|
+
flushTimer.unref();
|
|
99
|
+
|
|
100
|
+
// Shut down through postmortem rather than a bare signal listener. postmortem
|
|
101
|
+
// owns SIGINT/SIGTERM/SIGHUP/exit and quit(), and awaits registered cleanups
|
|
102
|
+
// before calling process.exit — so the batch processor's final OTLP export
|
|
103
|
+
// completes instead of being cut off mid-flight on the shutdown path.
|
|
104
|
+
postmortem.register("otel-trace-export", async () => {
|
|
105
|
+
clearInterval(flushTimer);
|
|
106
|
+
await provider?.shutdown();
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Parse the `OTEL_TRACES_EXPORTER` selection. The value is a case-insensitive,
|
|
112
|
+
* comma-separated list; the literal `none` disables span export entirely.
|
|
113
|
+
*/
|
|
114
|
+
function tracesExporterDisabled(raw: string | undefined): boolean {
|
|
115
|
+
if (!raw) return false;
|
|
116
|
+
return raw.split(",").some(entry => entry.trim().toLowerCase() === "none");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Flush any buffered spans to the exporter. No-op when export is disabled.
|
|
121
|
+
* Hosts embedding the agent can call this at natural boundaries (e.g. the end
|
|
122
|
+
* of a turn) so traces surface promptly rather than on the batch interval.
|
|
123
|
+
*/
|
|
124
|
+
export async function flushTelemetryExport(): Promise<void> {
|
|
125
|
+
await provider?.forceFlush();
|
|
126
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as Module from "node:module";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Bun's compiled-binary module resolver only finds `<pkg>/index.js` for bare
|
|
7
|
+
* specifiers loaded from the *real* filesystem — it ignores `main`/`exports`
|
|
8
|
+
* (issue #1763). The tiny-model Transformers.js runtime is `bun install`ed into
|
|
9
|
+
* a cache directory at runtime, and its graph (`@huggingface/transformers` →
|
|
10
|
+
* `onnxruntime-node` → `onnxruntime-common`, plus an eager `require("sharp")`)
|
|
11
|
+
* all point `main`/`exports` at nested files, so the stock resolver cannot load
|
|
12
|
+
* any of them. We patch `Module._resolveFilename` to resolve those bare
|
|
13
|
+
* specifiers against the cache ourselves, honoring `main`/`exports`.
|
|
14
|
+
*
|
|
15
|
+
* This module is filesystem-pure aside from {@link installRuntimeModuleResolver}
|
|
16
|
+
* mutating the `node:module` resolver, so the resolution logic is unit-testable
|
|
17
|
+
* without a compiled binary.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Conditions honored when resolving an `exports` map for a CommonJS `require`. */
|
|
21
|
+
const RUNTIME_CONDITIONS: Record<string, true> = { node: true, require: true, default: true };
|
|
22
|
+
|
|
23
|
+
/** Extension probes appended to a `main`/`exports` target that lacks one. */
|
|
24
|
+
const RUNTIME_EXTENSIONS: readonly string[] = [".js", ".cjs", ".mjs", ".json", ".node"];
|
|
25
|
+
|
|
26
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
27
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Walk a conditional `exports` target (string, array of fallbacks, or a
|
|
32
|
+
* condition object) and return the first relative path that matches a runtime
|
|
33
|
+
* condition in declaration order. Returns `null` when nothing applies (e.g.
|
|
34
|
+
* `import`-only targets).
|
|
35
|
+
*/
|
|
36
|
+
export function selectConditionalTarget(target: unknown): string | null {
|
|
37
|
+
if (typeof target === "string") return target;
|
|
38
|
+
if (Array.isArray(target)) {
|
|
39
|
+
for (const entry of target) {
|
|
40
|
+
const resolved = selectConditionalTarget(entry);
|
|
41
|
+
if (resolved) return resolved;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
if (isRecord(target)) {
|
|
46
|
+
for (const condition in target) {
|
|
47
|
+
if (!RUNTIME_CONDITIONS[condition]) continue;
|
|
48
|
+
const resolved = selectConditionalTarget(target[condition]);
|
|
49
|
+
if (resolved) return resolved;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Resolve a relative target inside a package to a concrete file path, probing extensions and `index`. */
|
|
56
|
+
function resolveFileTarget(pkgDir: string, relative: string): string | null {
|
|
57
|
+
const base = path.join(pkgDir, relative);
|
|
58
|
+
const candidates = [base, ...RUNTIME_EXTENSIONS.map(ext => base + ext)];
|
|
59
|
+
for (const candidate of candidates) {
|
|
60
|
+
try {
|
|
61
|
+
const stat = fs.statSync(candidate);
|
|
62
|
+
if (stat.isFile()) return candidate;
|
|
63
|
+
if (stat.isDirectory()) {
|
|
64
|
+
const indexed = resolveFileTarget(candidate, "index");
|
|
65
|
+
if (indexed) return indexed;
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// missing candidate — keep probing
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resolveExportsEntry(
|
|
75
|
+
pkgDir: string,
|
|
76
|
+
exports: Record<string, unknown>,
|
|
77
|
+
subpath: string | undefined,
|
|
78
|
+
): string | null {
|
|
79
|
+
let subpathMap = false;
|
|
80
|
+
for (const key in exports) {
|
|
81
|
+
subpathMap = key === "." || key.startsWith("./");
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
if (subpathMap) {
|
|
85
|
+
const key = subpath ? `./${subpath}` : ".";
|
|
86
|
+
if (!(key in exports)) return null;
|
|
87
|
+
const target = selectConditionalTarget(exports[key]);
|
|
88
|
+
return target ? resolveFileTarget(pkgDir, target) : null;
|
|
89
|
+
}
|
|
90
|
+
// A bare condition map only describes the package root, so a subpath
|
|
91
|
+
// request falls through to plain path joining at the call site.
|
|
92
|
+
if (subpath) return null;
|
|
93
|
+
const target = selectConditionalTarget(exports);
|
|
94
|
+
return target ? resolveFileTarget(pkgDir, target) : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Split a bare specifier into its package name and optional subpath, handling
|
|
99
|
+
* scoped packages (`@scope/name/sub` → `@scope/name` + `sub`).
|
|
100
|
+
*/
|
|
101
|
+
export function splitBareSpecifier(specifier: string): { packageName: string; subpath: string | undefined } {
|
|
102
|
+
const segments = specifier.split("/");
|
|
103
|
+
const take = specifier.startsWith("@") ? 2 : 1;
|
|
104
|
+
const packageName = segments.slice(0, take).join("/");
|
|
105
|
+
const subpath = segments.length > take ? segments.slice(take).join("/") : undefined;
|
|
106
|
+
return { packageName, subpath };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Resolve a bare specifier against an installed `node_modules` directory,
|
|
111
|
+
* honoring `exports` (CommonJS conditions), then `main`, then `index.js`.
|
|
112
|
+
* Returns an absolute file path, or `null` when the package/entry is absent.
|
|
113
|
+
*/
|
|
114
|
+
export function resolveRuntimeModule(runtimeNodeModules: string, specifier: string): string | null {
|
|
115
|
+
const { packageName, subpath } = splitBareSpecifier(specifier);
|
|
116
|
+
const pkgDir = path.join(runtimeNodeModules, ...packageName.split("/"));
|
|
117
|
+
const manifest = readManifest(pkgDir);
|
|
118
|
+
if (!manifest) return subpath ? resolveFileTarget(pkgDir, subpath) : null;
|
|
119
|
+
|
|
120
|
+
const { exports } = manifest;
|
|
121
|
+
if (typeof exports === "string" || isRecord(exports)) {
|
|
122
|
+
const map = typeof exports === "string" ? { ".": exports } : exports;
|
|
123
|
+
const resolved = resolveExportsEntry(pkgDir, map, subpath);
|
|
124
|
+
if (resolved) return resolved;
|
|
125
|
+
}
|
|
126
|
+
if (subpath) return resolveFileTarget(pkgDir, subpath);
|
|
127
|
+
if (typeof manifest.main === "string") {
|
|
128
|
+
const resolved = resolveFileTarget(pkgDir, manifest.main);
|
|
129
|
+
if (resolved) return resolved;
|
|
130
|
+
}
|
|
131
|
+
return resolveFileTarget(pkgDir, "index.js");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function readManifest(pkgDir: string): Record<string, unknown> | null {
|
|
135
|
+
try {
|
|
136
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(path.join(pkgDir, "package.json"), "utf8"));
|
|
137
|
+
return isRecord(parsed) ? parsed : null;
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface ModuleResolver {
|
|
144
|
+
_resolveFilename(request: string, parent: unknown, isMain: boolean, options?: unknown): string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const PATCHED = Symbol.for("omp.tiny.compiledRuntimeResolver");
|
|
148
|
+
|
|
149
|
+
interface ResolverOptions {
|
|
150
|
+
/** Absolute path to the runtime cache's `node_modules`. */
|
|
151
|
+
runtimeNodeModules: string;
|
|
152
|
+
/** Bare specifier → absolute file path overrides (e.g. `sharp` → no-op stub). */
|
|
153
|
+
stubs?: Record<string, string>;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Patch `node:module`'s resolver (idempotently) so bare specifiers that the
|
|
158
|
+
* stock compiled-binary resolver cannot find fall back to the runtime cache.
|
|
159
|
+
* Stock resolution is tried first, so this never changes behavior for modules
|
|
160
|
+
* that already resolve (the worker's own bundled imports, node builtins).
|
|
161
|
+
*/
|
|
162
|
+
export function installRuntimeModuleResolver({ runtimeNodeModules, stubs = {} }: ResolverOptions): void {
|
|
163
|
+
const resolver = (Module as unknown as { default?: ModuleResolver } & ModuleResolver).default ?? Module;
|
|
164
|
+
const target = resolver as unknown as ModuleResolver & { [PATCHED]?: boolean };
|
|
165
|
+
if (target[PATCHED]) return;
|
|
166
|
+
const original = target._resolveFilename.bind(target);
|
|
167
|
+
target._resolveFilename = (request: string, parent: unknown, isMain: boolean, options?: unknown): string => {
|
|
168
|
+
try {
|
|
169
|
+
return original(request, parent, isMain, options);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
const stub = stubs[request];
|
|
172
|
+
if (stub) return stub;
|
|
173
|
+
const resolved = resolveRuntimeModule(runtimeNodeModules, request);
|
|
174
|
+
if (resolved) return resolved;
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
target[PATCHED] = true;
|
|
179
|
+
}
|
package/src/tiny/worker.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
import { getTinyModelsCacheDir, isCompiledBinary, prompt } from "@oh-my-pi/pi-utils";
|
|
11
11
|
import packageJson from "../../package.json" with { type: "json" };
|
|
12
12
|
import tinyTitleSystemPrompt from "../prompts/system/tiny-title-system.md" with { type: "text" };
|
|
13
|
+
import { installRuntimeModuleResolver, resolveRuntimeModule } from "./compiled-runtime";
|
|
13
14
|
import { resolveTinyModelDevicePreference, type TinyModelDevice, tinyModelDeviceLoadOrder } from "./device";
|
|
14
15
|
import { resolveTinyModelDtypeOverride, type TinyModelDtype } from "./dtype";
|
|
15
16
|
import {
|
|
@@ -28,6 +29,7 @@ const STOP_DECODE_WINDOW_TOKENS = 32;
|
|
|
28
29
|
const MEMORY_COMPLETION_MAX_NEW_TOKENS = 256;
|
|
29
30
|
const TINY_TITLE_SYSTEM_PROMPT = prompt.render(tinyTitleSystemPrompt);
|
|
30
31
|
const TRANSFORMERS_PACKAGE = "@huggingface/transformers";
|
|
32
|
+
const COMPILED_TRANSFORMERS_VERSION = process.env.PI_TINY_TRANSFORMERS_VERSION;
|
|
31
33
|
const sourceRequire = createRequire(import.meta.url);
|
|
32
34
|
const INSTALL_LOCK_ATTEMPTS = 240;
|
|
33
35
|
const INSTALL_LOCK_SLEEP_MS = 250;
|
|
@@ -67,6 +69,7 @@ function resolveTransformersVersionSpec(): string {
|
|
|
67
69
|
manifest.optionalDependencies?.[TRANSFORMERS_PACKAGE] ?? manifest.dependencies?.[TRANSFORMERS_PACKAGE];
|
|
68
70
|
if (!versionSpec) throw new Error(`${TRANSFORMERS_PACKAGE} is missing from package.json optionalDependencies`);
|
|
69
71
|
if (!versionSpec.startsWith("catalog:")) return versionSpec;
|
|
72
|
+
if (COMPILED_TRANSFORMERS_VERSION) return COMPILED_TRANSFORMERS_VERSION;
|
|
70
73
|
const installed = sourceRequire(`${TRANSFORMERS_PACKAGE}/package.json`) as { version: string };
|
|
71
74
|
return installed.version;
|
|
72
75
|
}
|
|
@@ -117,6 +120,7 @@ function getTinyTitleRuntimeDir(): string {
|
|
|
117
120
|
|
|
118
121
|
async function acquireInstallLock(runtimeDir: string): Promise<() => Promise<void>> {
|
|
119
122
|
const lockDir = `${runtimeDir}.lock`;
|
|
123
|
+
await fs.mkdir(path.dirname(lockDir), { recursive: true });
|
|
120
124
|
for (let attempt = 0; attempt < INSTALL_LOCK_ATTEMPTS; attempt++) {
|
|
121
125
|
try {
|
|
122
126
|
await fs.mkdir(lockDir);
|
|
@@ -216,6 +220,23 @@ async function ensureCompiledTransformersRuntime(
|
|
|
216
220
|
}
|
|
217
221
|
}
|
|
218
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Prepare the freshly-installed compiled runtime for loading: stub `sharp`
|
|
225
|
+
* (the tiny models are text-generation only, so the native image pipeline is
|
|
226
|
+
* dead weight) and patch the module resolver so Transformers.js's bare requires
|
|
227
|
+
* (`onnxruntime-node`, `onnxruntime-common`) resolve against the cache. Returns
|
|
228
|
+
* the absolute Transformers.js entrypoint to `require`.
|
|
229
|
+
*/
|
|
230
|
+
async function prepareCompiledRuntime(runtimeDir: string): Promise<string> {
|
|
231
|
+
const nodeModules = path.join(runtimeDir, "node_modules");
|
|
232
|
+
const sharpStub = path.join(runtimeDir, "omp-sharp-stub.cjs");
|
|
233
|
+
await Bun.write(sharpStub, "module.exports = {};\n");
|
|
234
|
+
installRuntimeModuleResolver({ runtimeNodeModules: nodeModules, stubs: { sharp: sharpStub } });
|
|
235
|
+
const entry = resolveRuntimeModule(nodeModules, TRANSFORMERS_PACKAGE);
|
|
236
|
+
if (!entry) throw new Error(`Unable to resolve ${TRANSFORMERS_PACKAGE} in compiled runtime at ${nodeModules}`);
|
|
237
|
+
return entry;
|
|
238
|
+
}
|
|
239
|
+
|
|
219
240
|
function configureTransformers(transformers: TransformersRuntime): TransformersRuntime {
|
|
220
241
|
transformers.env.cacheDir = getTinyModelsCacheDir();
|
|
221
242
|
transformers.env.allowLocalModels = false;
|
|
@@ -232,8 +253,9 @@ async function loadTransformers(
|
|
|
232
253
|
transformersRuntime = (async () => {
|
|
233
254
|
if (!isCompiledBinary()) return configureTransformers(sourceRequire(TRANSFORMERS_PACKAGE) as TransformersRuntime);
|
|
234
255
|
const runtimeDir = await ensureCompiledTransformersRuntime(transport, requestId, modelKey);
|
|
235
|
-
const
|
|
236
|
-
|
|
256
|
+
const entry = await prepareCompiledRuntime(runtimeDir);
|
|
257
|
+
const require_ = createRequire(entry);
|
|
258
|
+
return configureTransformers(require_(entry) as TransformersRuntime);
|
|
237
259
|
})().catch(error => {
|
|
238
260
|
transformersRuntime = null;
|
|
239
261
|
throw error;
|