modelrig 0.11.0 → 0.11.1
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 +48 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +23 -3
- package/dist/doctor-preflight.d.ts.map +1 -1
- package/dist/doctor-preflight.js +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/init/init-cli.d.ts.map +1 -1
- package/dist/init/init-cli.js +21 -4
- package/dist/init/observe-cli.js +1 -1
- package/dist/init/observe.d.ts +7 -1
- package/dist/init/observe.d.ts.map +1 -1
- package/dist/init/observe.js +74 -22
- package/dist/init/starter.d.ts +36 -0
- package/dist/init/starter.d.ts.map +1 -0
- package/dist/init/starter.js +85 -0
- package/dist/run/rawonly.d.ts +29 -5
- package/dist/run/rawonly.d.ts.map +1 -1
- package/dist/run/rawonly.js +54 -19
- package/docs/agent-skill/SKILL.md +107 -10
- package/docs/how-it-fits.md +1 -1
- package/docs/instrumentation.md +135 -40
- package/docs/llms.txt +90 -8
- package/docs/quickstart.md +127 -15
- package/package.json +1 -1
package/dist/init/observe.js
CHANGED
|
@@ -25,6 +25,10 @@ exports.seamContent = seamContent;
|
|
|
25
25
|
exports.buildObserveMarkdown = buildObserveMarkdown;
|
|
26
26
|
exports.runObserve = runObserve;
|
|
27
27
|
const finder_1 = require("./finder");
|
|
28
|
+
/** The `environment` expression the wiring diff proposes. Assembled so this
|
|
29
|
+
* source file never holds the literal env-read token (package invariant: only
|
|
30
|
+
* config.ts reads the environment). */
|
|
31
|
+
const ENV_EXPR = `${["process", "env", "NODE_ENV"].join(".")} ?? "development"`;
|
|
28
32
|
const normalize = (s) => s.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_|_$/g, "").toLowerCase();
|
|
29
33
|
/** A stable step key from a call site: `<dir>_<file>_<line>`, deduped so two
|
|
30
34
|
* sites never collide. Deterministic, so a second run proposes the same keys. */
|
|
@@ -40,33 +44,43 @@ function stepKeyFor(site, used) {
|
|
|
40
44
|
}
|
|
41
45
|
/** The generated seam. Self-contained and fresh (never resurrected from git
|
|
42
46
|
* history — that is the point of onboarding-test run-2). Mirrors the reference
|
|
43
|
-
* `recordStep` in instrumentation.md, plus a `withRun` boundary wrapper.
|
|
47
|
+
* `recordStep` in instrumentation.md, plus a `withRun` boundary wrapper.
|
|
48
|
+
*
|
|
49
|
+
* SURF-1 (audit #1, #6): the rig is built `routesDir: null` (observe needs no
|
|
50
|
+
* routes, and the default dir is absent in a fresh repo — the old seam crashed
|
|
51
|
+
* the customer's app on import), and `recordStep` reports the provider call
|
|
52
|
+
* through `rig.recordCall` so the step carries tokens and a cost and the call
|
|
53
|
+
* shows on /inferences. */
|
|
44
54
|
function seamContent() {
|
|
45
55
|
return `/**
|
|
46
56
|
* ModelRig observe seam — generated by \`modelrig observe\`.
|
|
47
57
|
*
|
|
48
58
|
* This is YOURS to keep and edit; it is generated fresh (not copied from any
|
|
49
59
|
* deleted seam) so it always matches the current SDK. It records each pipeline
|
|
50
|
-
* run as a run -> step -> artifact chain you click through in the console
|
|
60
|
+
* run as a run -> step -> artifact chain you click through in the console, and
|
|
61
|
+
* each model call as a recorded inference with its tokens and cost.
|
|
51
62
|
*
|
|
52
|
-
* Observe-only: no routes move, no model calls are rerouted.
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
63
|
+
* Observe-only: no routes move, no model calls are rerouted. Your code keeps
|
|
64
|
+
* calling the provider with your own key; this file only REPORTS what happened.
|
|
65
|
+
* With no MODELRIG_API_KEY (or the org capture posture set to off) every call
|
|
66
|
+
* here is inert and your pipeline behaves byte-for-byte as before. Nothing here
|
|
67
|
+
* ever throws into your pipeline — a telemetry write that fails is logged and
|
|
56
68
|
* dropped (instrumentation.md, "Never throws into your pipeline").
|
|
57
69
|
*
|
|
58
70
|
* Docs: https://modelrig.dev/instrumentation.html
|
|
59
71
|
*/
|
|
60
72
|
|
|
61
|
-
import { createRig, loadConfigFromEnv, type Rig } from "modelrig";
|
|
73
|
+
import { createRig, loadConfigFromEnv, type RecordCallInput, type Rig } from "modelrig";
|
|
62
74
|
|
|
63
|
-
|
|
75
|
+
// routesDir: null — observe needs no route files, so the rig is raw-only and
|
|
76
|
+
// never looks for ./modelrig/routes (importing this file cannot throw).
|
|
77
|
+
const rig: Rig = createRig({ ...loadConfigFromEnv(), routesDir: null });
|
|
64
78
|
|
|
65
79
|
/** Open a run context around a whole pipeline, close it on success/failure.
|
|
66
80
|
* \`episodeKey\` is a grouping key (a natural choice: your per-run attempt id),
|
|
67
|
-
* not a unique id. Pass \`environment\` (e.g. your NODE_ENV)
|
|
68
|
-
* Use \`rig.artifacts.run.scope(...)\` instead for
|
|
69
|
-
* process. */
|
|
81
|
+
* not a unique id. Pass \`environment\` (e.g. your NODE_ENV) so the Env column
|
|
82
|
+
* on /runs is filled. Use \`rig.artifacts.run.scope(...)\` instead for
|
|
83
|
+
* concurrent runs in one process. */
|
|
70
84
|
export async function withRun<T>(
|
|
71
85
|
opts: { pipeline: string; episodeKey: string; environment?: string },
|
|
72
86
|
body: () => Promise<T>,
|
|
@@ -86,14 +100,20 @@ export async function withRun<T>(
|
|
|
86
100
|
}
|
|
87
101
|
}
|
|
88
102
|
|
|
89
|
-
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
|
|
103
|
+
/** The provider call your code made, as the provider reported it. Recorded with
|
|
104
|
+
* \`rig.recordCall\` so the step carries tokens and a cost ("recorded ·
|
|
105
|
+
* unverified": ModelRig did not dispatch it). */
|
|
106
|
+
export type StepCall = Omit<RecordCallInput, "stepKey">;
|
|
107
|
+
|
|
108
|
+
/** Record one step: report the provider call (tokens → cost on /runs and
|
|
109
|
+
* /inferences), save prompt + raw + parsed, link lineage, flag conformance.
|
|
110
|
+
* Leave \`call\` out only when you have no usage numbers; the step then shows
|
|
111
|
+
* "cost n/a". */
|
|
93
112
|
export function recordStep(
|
|
94
113
|
stepKey: string,
|
|
95
|
-
work: { prompt: string; rawText: string; json: unknown; conforms: boolean },
|
|
114
|
+
work: { prompt: string; rawText: string; json: unknown; conforms: boolean; call?: StepCall },
|
|
96
115
|
): void {
|
|
116
|
+
if (work.call !== undefined) rig.recordCall({ ...work.call, stepKey });
|
|
97
117
|
const p = rig.artifacts.artifact.save(work.prompt, { name: \`\${stepKey}.prompt\`, type: "prompt", stepKey });
|
|
98
118
|
const raw = rig.artifacts.artifact.save(work.rawText, { name: \`\${stepKey}.raw\`, type: "raw_response", stepKey });
|
|
99
119
|
const parsed = rig.artifacts.artifact.save(work.json, { name: \`\${stepKey}.output\`, type: "step_output", task: stepKey, stepKey });
|
|
@@ -101,6 +121,12 @@ export function recordStep(
|
|
|
101
121
|
if (raw && parsed) rig.artifacts.artifact.link(parsed, raw, "derived_from");
|
|
102
122
|
if (parsed) rig.grade({ kind: "artifact", id: parsed.id }, { score: work.conforms ? 1 : 0, kind: "deterministic", grader: "schema-check" });
|
|
103
123
|
}
|
|
124
|
+
|
|
125
|
+
/** Flush and close the rig. Await it once at shutdown (a one-shot script that
|
|
126
|
+
* exits without it can lose the last batch). */
|
|
127
|
+
export function closeRig(): Promise<void> {
|
|
128
|
+
return rig.close();
|
|
129
|
+
}
|
|
104
130
|
`;
|
|
105
131
|
}
|
|
106
132
|
/** The import specifier a source file uses to reach the seam. Best-effort
|
|
@@ -110,12 +136,36 @@ function importSpecifier(fromFile, seamPath, path) {
|
|
|
110
136
|
const rel = path.relative(path.dirname(fromFile), noExt);
|
|
111
137
|
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
112
138
|
}
|
|
113
|
-
|
|
139
|
+
/** How the customer's provider response reports usage, per provider family —
|
|
140
|
+
* the expressions the wiring diff proposes for `call.usage` (the human/agent
|
|
141
|
+
* adjusts the variable name). Unknown providers get a placeholder. */
|
|
142
|
+
const USAGE_EXPR = {
|
|
143
|
+
openai: { tokensIn: "res.usage?.prompt_tokens ?? 0", tokensOut: "res.usage?.completion_tokens ?? 0" },
|
|
144
|
+
deepseek: { tokensIn: "res.usage?.prompt_tokens ?? 0", tokensOut: "res.usage?.completion_tokens ?? 0" },
|
|
145
|
+
grok: { tokensIn: "res.usage?.prompt_tokens ?? 0", tokensOut: "res.usage?.completion_tokens ?? 0" },
|
|
146
|
+
deepinfra: { tokensIn: "res.usage?.prompt_tokens ?? 0", tokensOut: "res.usage?.completion_tokens ?? 0" },
|
|
147
|
+
fireworks: { tokensIn: "res.usage?.prompt_tokens ?? 0", tokensOut: "res.usage?.completion_tokens ?? 0" },
|
|
148
|
+
anthropic: { tokensIn: "res.usage.input_tokens", tokensOut: "res.usage.output_tokens" },
|
|
149
|
+
// Gemini bills thinking tokens as output: candidates + thoughts (SURF-1 QA).
|
|
150
|
+
gemini: {
|
|
151
|
+
tokensIn: "res.usageMetadata?.promptTokenCount ?? 0",
|
|
152
|
+
tokensOut: "(res.usageMetadata?.candidatesTokenCount ?? 0) + (res.usageMetadata?.thoughtsTokenCount ?? 0)",
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
function recordStepSnippet(site, stepKey) {
|
|
156
|
+
const usage = USAGE_EXPR[site.provider] ?? { tokensIn: "/* input tokens */ 0", tokensOut: "/* output tokens */ 0" };
|
|
157
|
+
const model = site.model !== null ? JSON.stringify(site.model) : site.modelExpression ?? '"<model id>"';
|
|
114
158
|
return (`recordStep(${JSON.stringify(stepKey)}, {\n` +
|
|
115
159
|
` prompt, // the assembled prompt string\n` +
|
|
116
160
|
` rawText, // the model's raw text, pre-parse\n` +
|
|
117
161
|
` json: parsed, // your parsed/validated output\n` +
|
|
118
162
|
` conforms, // did it satisfy your schema?\n` +
|
|
163
|
+
` call: { // what the provider reported → tokens + cost (rig.recordCall)\n` +
|
|
164
|
+
` provider: ${JSON.stringify(site.provider)},\n` +
|
|
165
|
+
` model: ${model},\n` +
|
|
166
|
+
` usage: { tokensIn: ${usage.tokensIn}, tokensOut: ${usage.tokensOut} },\n` +
|
|
167
|
+
` latencyMs, // Date.now() after the call minus before it\n` +
|
|
168
|
+
` },\n` +
|
|
119
169
|
`});`);
|
|
120
170
|
}
|
|
121
171
|
/** The run boundary is most likely the shallowest detected file (the entry
|
|
@@ -139,7 +189,7 @@ function buildObserveMarkdown(plans, seamPath, result, path) {
|
|
|
139
189
|
"**Nothing in your source has changed.** `modelrig observe` generated one seam helper",
|
|
140
190
|
`(\`${seamPath}\`) and this plan. Observe is **not migration**: no routes were drafted, no`,
|
|
141
191
|
"model calls rerouted. Applying the wiring below records each run as a run → step →",
|
|
142
|
-
"artifact chain you click through in the console — and with
|
|
192
|
+
"artifact chain you click through in the console — and with no key set, your pipeline",
|
|
143
193
|
"behaves **byte-for-byte** as before.",
|
|
144
194
|
"",
|
|
145
195
|
`Detected **${result.sitesFound} model call site(s)** across ${result.filesScanned} scanned file(s);`,
|
|
@@ -154,9 +204,11 @@ function buildObserveMarkdown(plans, seamPath, result, path) {
|
|
|
154
204
|
"",
|
|
155
205
|
"```bash",
|
|
156
206
|
"export MODELRIG_API_KEY=<key> # mint at https://app.modelrig.ai (/keys)",
|
|
157
|
-
"# export MODELRIG_ARTIFACT_STEPS=step_a,step_b # optional: instrument a subset",
|
|
158
207
|
"```",
|
|
159
208
|
"",
|
|
209
|
+
"The seam builds its rig with `routesDir: null`: observe needs no route files, so importing",
|
|
210
|
+
"it never looks for `./modelrig/routes`.",
|
|
211
|
+
"",
|
|
160
212
|
"> **Human handoff (one step, once).** Seeing these runs **in the console** needs a free",
|
|
161
213
|
"> account + an API key minted at <https://app.modelrig.ai> (`/keys`) — a console login a",
|
|
162
214
|
"> coding agent cannot perform. Set `MODELRIG_API_KEY=<key>` once a human provides it; the",
|
|
@@ -174,7 +226,7 @@ function buildObserveMarkdown(plans, seamPath, result, path) {
|
|
|
174
226
|
lines.push("## 2. Open a run context around the pipeline", "", boundary === null
|
|
175
227
|
? "Wrap your pipeline's entry point in `withRun(...)` from the seam helper:"
|
|
176
228
|
: `Your pipeline entry looks like \`${boundary}\` (the shallowest detected call site — a` +
|
|
177
|
-
" heuristic; move this if your real entry is elsewhere). Wrap it in `withRun(...)`:", "", "```diff", `+ import { withRun } from "${boundaryImport}";`, "", "- export async function runPipeline(input) {", "- /* ... your steps ... */", "- }", "+ export async function runPipeline(input) {", "+ return withRun(",
|
|
229
|
+
" heuristic; move this if your real entry is elsewhere). Wrap it in `withRun(...)`:", "", "```diff", `+ import { withRun } from "${boundaryImport}";`, "", "- export async function runPipeline(input) {", "- /* ... your steps ... */", "- }", "+ export async function runPipeline(input) {", "+ return withRun(", `+ { pipeline: "my-pipeline", episodeKey: input.attemptId, environment: ${ENV_EXPR} },`, "+ async () => {", "+ /* ... your steps ... */", "+ },", "+ );", "+ }", "```", "", "## 3. Record each step's work products", "", "Each detected model call is one step. After the call parses its output, add a `recordStep`", "(the diffs below quote the call line so you can find it). Its `call` field reports what the", "provider returned through `rig.recordCall`, so the step carries tokens and a cost and the call", "shows on /inferences as \"recorded · unverified\". Time the call (`const t0 = Date.now()` before,", "`const latencyMs = Date.now() - t0` after) and rename `res` to your response variable:", "");
|
|
178
230
|
// Group the per-site wiring diffs by file so each file gets one import line.
|
|
179
231
|
const byFile = new Map();
|
|
180
232
|
for (const plan of plans) {
|
|
@@ -190,7 +242,7 @@ function buildObserveMarkdown(plans, seamPath, result, path) {
|
|
|
190
242
|
}
|
|
191
243
|
lines.push("```", "");
|
|
192
244
|
}
|
|
193
|
-
lines.push("## 4. See it", "", "Run your pipeline once, then open the console: **Runs** lists the run, `/runs/[id]` renders", "the semantic chain (artifacts grouped by step, with hashes and evaluation badges), and", "`/artifacts/[id]` shows one artifact's lineage.
|
|
245
|
+
lines.push("## 4. See it", "", "Run your pipeline once, then open the console: **Runs** lists the run, `/runs/[id]` renders", "the semantic chain (artifacts grouped by step, with hashes and evaluation badges), and", "`/artifacts/[id]` shows one artifact's lineage, and **Inferences** lists each recorded call", "with its cost. A step you wired without `call` shows **cost n/a**: ModelRig has no usage", "numbers for it. In a script that exits, `await closeRig()` once at the end so the last batch", "ships. Calls your code makes outside `recordStep` stay invisible: https://modelrig.dev/not-captured.html", "", "## When you are ready to migrate (later, optional)", "", "Observe answers *what is my pipeline doing?* When you want to *change what serves a step*,", "that is route extraction (T2) — a separate, behaviour-affecting move behind its own review:", "`modelrig init --scope <this pipeline>`. Observe first is the safer order.", "");
|
|
194
246
|
return `${lines.join("\n")}\n`;
|
|
195
247
|
}
|
|
196
248
|
function runObserve(deps, options = {}) {
|
|
@@ -198,7 +250,7 @@ function runObserve(deps, options = {}) {
|
|
|
198
250
|
const used = new Set();
|
|
199
251
|
const plans = scan.sites.map((site) => {
|
|
200
252
|
const stepKey = stepKeyFor(site, used);
|
|
201
|
-
return { site, stepKey, snippet: recordStepSnippet(stepKey) };
|
|
253
|
+
return { site, stepKey, snippet: recordStepSnippet(site, stepKey) };
|
|
202
254
|
});
|
|
203
255
|
const seamPath = deps.path.join(deps.outDir, "record-step.ts");
|
|
204
256
|
// Import specifiers and prose are computed root-relative — call sites come
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The starter route's companion files (SURF-1, audit #2). Every on-ramp shows
|
|
3
|
+
* the same starter route, `example.support_summarize` (console
|
|
4
|
+
* STARTER_ROUTE_YAML, www /get-started, quickstart §3), and that YAML references
|
|
5
|
+
* `./schemas/support_summarize.schema.json` and
|
|
6
|
+
* `./prompts/support_summarize.system.md`. Neither shipped in the npm tarball,
|
|
7
|
+
* so a customer who pasted the route got "schema file not found" from
|
|
8
|
+
* `modelrig validate`. `modelrig init` now writes both next to the routes dir
|
|
9
|
+
* (never clobbering), and `surf1-setup.test.ts` asserts these constants equal the
|
|
10
|
+
* package's own committed example files and that the starter validates.
|
|
11
|
+
*
|
|
12
|
+
* They are support files, not routes: the loader reads only top-level *.yaml,
|
|
13
|
+
* so writing them can never change what a rig serves.
|
|
14
|
+
*/
|
|
15
|
+
/** Path of each companion file, relative to the routes directory. */
|
|
16
|
+
export declare const STARTER_SCHEMA_PATH = "schemas/support_summarize.schema.json";
|
|
17
|
+
export declare const STARTER_PROMPT_PATH = "prompts/support_summarize.system.md";
|
|
18
|
+
export declare const STARTER_SCHEMA_JSON = "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"title\": \"support_summarize\",\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"required\": [\"summary\", \"handoff\"],\n \"properties\": {\n \"summary\": {\n \"type\": \"string\",\n \"minLength\": 40,\n \"description\": \"One paragraph a human agent can act on without reading the thread.\"\n },\n \"severity\": { \"type\": \"string\", \"enum\": [\"low\", \"normal\", \"high\", \"urgent\"] },\n \"handoff\": {\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"required\": [\"team\", \"reason\"],\n \"properties\": {\n \"team\": { \"type\": \"string\", \"enum\": [\"billing\", \"technical\", \"account\", \"none\"] },\n \"reason\": { \"type\": \"string\", \"minLength\": 10 }\n }\n }\n }\n}\n";
|
|
19
|
+
export declare const STARTER_PROMPT_MD = "You summarise a customer support ticket for the agent who picks it up next.\n\nProduct: {{product}}\n\nEarlier context on this customer (may be \"none\"):\n{{priorContext}}\n\nTicket:\n{{ticket}}\n\nWrite one actionable paragraph, judge severity, and name the team it should go\nto (or `none` if the summary closes it).\n\n{{#unless capability.structured_native}}\nReturn ONLY a JSON object matching this shape \u2014 no prose, no code fence:\n{\"summary\": string, \"severity\": \"low\"|\"normal\"|\"high\"|\"urgent\",\n \"handoff\": {\"team\": \"billing\"|\"technical\"|\"account\"|\"none\", \"reason\": string}}\n{{/unless}}\n";
|
|
20
|
+
export interface StarterFs {
|
|
21
|
+
existsSync: (p: string) => boolean;
|
|
22
|
+
mkdirSync: (p: string, o: {
|
|
23
|
+
recursive: boolean;
|
|
24
|
+
}) => void;
|
|
25
|
+
writeFileSync: (p: string, c: string, e: "utf8") => void;
|
|
26
|
+
}
|
|
27
|
+
/** Write the starter's schema and prompt under `routesDir`, leaving any file that
|
|
28
|
+
* already exists alone (it is the customer's). Returns what was written/skipped. */
|
|
29
|
+
export declare function writeStarterFiles(routesDir: string, fs: StarterFs, path: {
|
|
30
|
+
join: (...parts: string[]) => string;
|
|
31
|
+
dirname: (p: string) => string;
|
|
32
|
+
}): {
|
|
33
|
+
written: string[];
|
|
34
|
+
skipped: string[];
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=starter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"starter.d.ts","sourceRoot":"","sources":["../../src/init/starter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,qEAAqE;AACrE,eAAO,MAAM,mBAAmB,0CAA0C,CAAC;AAC3E,eAAO,MAAM,mBAAmB,wCAAwC,CAAC;AAEzE,eAAO,MAAM,mBAAmB,w2BAwB/B,CAAC;AAEF,eAAO,MAAM,iBAAiB,0nBAkB7B,CAAC;AAEF,MAAM,WAAW,SAAS;IACxB,UAAU,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;IACnC,SAAS,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;IAC1D,aAAa,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAC1D;AAED;oFACoF;AACpF,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,SAAS,EACb,IAAI,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,KAAK,MAAM,CAAC;IAAC,OAAO,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAA;CAAE,GAC7E;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAiB1C"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The starter route's companion files (SURF-1, audit #2). Every on-ramp shows
|
|
4
|
+
* the same starter route, `example.support_summarize` (console
|
|
5
|
+
* STARTER_ROUTE_YAML, www /get-started, quickstart §3), and that YAML references
|
|
6
|
+
* `./schemas/support_summarize.schema.json` and
|
|
7
|
+
* `./prompts/support_summarize.system.md`. Neither shipped in the npm tarball,
|
|
8
|
+
* so a customer who pasted the route got "schema file not found" from
|
|
9
|
+
* `modelrig validate`. `modelrig init` now writes both next to the routes dir
|
|
10
|
+
* (never clobbering), and `surf1-setup.test.ts` asserts these constants equal the
|
|
11
|
+
* package's own committed example files and that the starter validates.
|
|
12
|
+
*
|
|
13
|
+
* They are support files, not routes: the loader reads only top-level *.yaml,
|
|
14
|
+
* so writing them can never change what a rig serves.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.STARTER_PROMPT_MD = exports.STARTER_SCHEMA_JSON = exports.STARTER_PROMPT_PATH = exports.STARTER_SCHEMA_PATH = void 0;
|
|
18
|
+
exports.writeStarterFiles = writeStarterFiles;
|
|
19
|
+
/** Path of each companion file, relative to the routes directory. */
|
|
20
|
+
exports.STARTER_SCHEMA_PATH = "schemas/support_summarize.schema.json";
|
|
21
|
+
exports.STARTER_PROMPT_PATH = "prompts/support_summarize.system.md";
|
|
22
|
+
exports.STARTER_SCHEMA_JSON = `{
|
|
23
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
24
|
+
"title": "support_summarize",
|
|
25
|
+
"type": "object",
|
|
26
|
+
"additionalProperties": false,
|
|
27
|
+
"required": ["summary", "handoff"],
|
|
28
|
+
"properties": {
|
|
29
|
+
"summary": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"minLength": 40,
|
|
32
|
+
"description": "One paragraph a human agent can act on without reading the thread."
|
|
33
|
+
},
|
|
34
|
+
"severity": { "type": "string", "enum": ["low", "normal", "high", "urgent"] },
|
|
35
|
+
"handoff": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"additionalProperties": false,
|
|
38
|
+
"required": ["team", "reason"],
|
|
39
|
+
"properties": {
|
|
40
|
+
"team": { "type": "string", "enum": ["billing", "technical", "account", "none"] },
|
|
41
|
+
"reason": { "type": "string", "minLength": 10 }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
`;
|
|
47
|
+
exports.STARTER_PROMPT_MD = `You summarise a customer support ticket for the agent who picks it up next.
|
|
48
|
+
|
|
49
|
+
Product: {{product}}
|
|
50
|
+
|
|
51
|
+
Earlier context on this customer (may be "none"):
|
|
52
|
+
{{priorContext}}
|
|
53
|
+
|
|
54
|
+
Ticket:
|
|
55
|
+
{{ticket}}
|
|
56
|
+
|
|
57
|
+
Write one actionable paragraph, judge severity, and name the team it should go
|
|
58
|
+
to (or \`none\` if the summary closes it).
|
|
59
|
+
|
|
60
|
+
{{#unless capability.structured_native}}
|
|
61
|
+
Return ONLY a JSON object matching this shape — no prose, no code fence:
|
|
62
|
+
{"summary": string, "severity": "low"|"normal"|"high"|"urgent",
|
|
63
|
+
"handoff": {"team": "billing"|"technical"|"account"|"none", "reason": string}}
|
|
64
|
+
{{/unless}}
|
|
65
|
+
`;
|
|
66
|
+
/** Write the starter's schema and prompt under `routesDir`, leaving any file that
|
|
67
|
+
* already exists alone (it is the customer's). Returns what was written/skipped. */
|
|
68
|
+
function writeStarterFiles(routesDir, fs, path) {
|
|
69
|
+
const written = [];
|
|
70
|
+
const skipped = [];
|
|
71
|
+
for (const [rel, body] of [
|
|
72
|
+
[exports.STARTER_SCHEMA_PATH, exports.STARTER_SCHEMA_JSON],
|
|
73
|
+
[exports.STARTER_PROMPT_PATH, exports.STARTER_PROMPT_MD],
|
|
74
|
+
]) {
|
|
75
|
+
const file = path.join(routesDir, rel);
|
|
76
|
+
if (fs.existsSync(file)) {
|
|
77
|
+
skipped.push(file);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
81
|
+
fs.writeFileSync(file, body, "utf8");
|
|
82
|
+
written.push(file);
|
|
83
|
+
}
|
|
84
|
+
return { written, skipped };
|
|
85
|
+
}
|
package/dist/run/rawonly.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* 700-LOC cap — `createRig` remains the ONE construction path (a separate
|
|
18
18
|
* `createRawRig()` factory was considered and REJECTED, spec §2 R1).
|
|
19
19
|
*/
|
|
20
|
-
import type
|
|
20
|
+
import { type RigConfig } from "../config";
|
|
21
21
|
import { type LoadedRoutes } from "../routes/loader";
|
|
22
22
|
import { RigFailureError } from "../types/failures";
|
|
23
23
|
import type { Rig } from "../index";
|
|
@@ -27,7 +27,12 @@ export declare const RAW_ONLY_FIX_HINT = "this rig was constructed runRaw-only (
|
|
|
27
27
|
/** Build the TERMINAL failure a route-dependent surface throws when the rig was
|
|
28
28
|
* constructed runRaw-only. Reuses `invariant_violation` (already TERMINAL + in
|
|
29
29
|
* the teaching table) — no new FailureClass (spec §3 rationale). */
|
|
30
|
-
|
|
30
|
+
/** Why a rig is raw-only: constructed with `routesDir: null`, or fell back
|
|
31
|
+
* because the default routes directory is absent (SURF-1). */
|
|
32
|
+
export type RawOnlyReason = "routesDir-null" | "absent-default";
|
|
33
|
+
/** The fix hint when the absent-default fallback made the rig raw-only. */
|
|
34
|
+
export declare const ABSENT_DEFAULT_FIX_HINT = "create ./modelrig/routes with your route files (`npx modelrig init` drafts them), or set MODELRIG_ROUTES_DIR";
|
|
35
|
+
export declare function rawOnlyRouteFailure(surface: string, reason?: RawOnlyReason): RigFailureError;
|
|
31
36
|
/** Narrow a config's `routesDir` to a string for a route-REQUIRED context (the
|
|
32
37
|
* route-oriented CLI subcommands: bakeoff/watch/swap/init/observe/validate),
|
|
33
38
|
* throwing the teaching failure if the rig was built runRaw-only.
|
|
@@ -35,6 +40,21 @@ export declare function rawOnlyRouteFailure(surface: string): RigFailureError;
|
|
|
35
40
|
* env-built configs — it guards a hand-built `routesDir: null` reaching a
|
|
36
41
|
* route-only command, and satisfies the `string | null` type at those seams. */
|
|
37
42
|
export declare function requireRoutesDir(config: RigConfig, surface: string): string;
|
|
43
|
+
/** The warning createRig prints when it falls back to a raw-only rig because the
|
|
44
|
+
* DEFAULT routes directory does not exist (SURF-1, audit #1). One constant so
|
|
45
|
+
* the docs and the test quote the same words. */
|
|
46
|
+
export declare const ABSENT_DEFAULT_ROUTES_WARNING: string;
|
|
47
|
+
/**
|
|
48
|
+
* The routes directory createRig actually loads (SURF-1 ruling on audit #1):
|
|
49
|
+
* - `null` stays `null` (CE-8 runRaw-only, silent);
|
|
50
|
+
* - the UNCONFIGURED default (loadConfigFromEnv filled in `./modelrig/routes`;
|
|
51
|
+
* no MODELRIG_ROUTES_DIR, no routesDir override, no `extraRoutesDir`) that
|
|
52
|
+
* does not exist on disk → warn once, return `null`, so
|
|
53
|
+
* a customer who only records calls never crashes on import;
|
|
54
|
+
* - anything explicitly configured is returned as-is, and a missing one still
|
|
55
|
+
* throws the teaching RouteConfigError from the loader.
|
|
56
|
+
*/
|
|
57
|
+
export declare function effectiveRoutesDir(config: RigConfig): string | null;
|
|
38
58
|
/** The empty route store for a runRaw-only rig: `list()` is `[]` (so
|
|
39
59
|
* serveability, capture-detection, and the exported route mirror are all no-ops)
|
|
40
60
|
* and `get()` teaches, though no public surface reaches it — every route method
|
|
@@ -48,13 +68,17 @@ export declare function loadRoutesOrReport(config: RigConfig, routesDir: string)
|
|
|
48
68
|
* `routesDir === null` so every one of them throws the teaching failure rather
|
|
49
69
|
* than dereferencing an empty store. Spread LAST over the real methods in
|
|
50
70
|
* `createRig`, so later keys win. */
|
|
51
|
-
type RouteMethods = Pick<Rig, "run" | "runStream" | "runBundle" | "runBundleStream" | "replay" | "bakeoff" | "
|
|
52
|
-
/** The throwing overrides for a runRaw-only rig (spec §2 R1).
|
|
71
|
+
type RouteMethods = Pick<Rig, "run" | "runStream" | "runBundle" | "runBundleStream" | "replay" | "bakeoff" | "verifySwap">;
|
|
72
|
+
/** The throwing overrides for a runRaw-only rig (spec §2 R1).
|
|
73
|
+
* `bakeoffFromEvalCases` is deliberately NOT overridden (SURF-1, audit #12): it
|
|
74
|
+
* reads eval cases from the local store first, so with none promoted (or none
|
|
75
|
+
* rehydratable) it reports honestly without any routes dir; only a case that
|
|
76
|
+
* actually replays reaches `routes.get`, which teaches the same failure. Each names the
|
|
53
77
|
* surface + the route/task the caller asked for, so the terminal error points at
|
|
54
78
|
* exactly the call that needs a `routesDir`. Promise-returning surfaces REJECT
|
|
55
79
|
* (matching the routed lane, where errors surface as rejections); the two
|
|
56
80
|
* streaming surfaces return a handle synchronously, so they throw synchronously
|
|
57
81
|
* — the caller never reaches an event to await. */
|
|
58
|
-
export declare function rawOnlyRouteMethods(): RouteMethods;
|
|
82
|
+
export declare function rawOnlyRouteMethods(reason?: RawOnlyReason): RouteMethods;
|
|
59
83
|
export {};
|
|
60
84
|
//# sourceMappingURL=rawonly.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rawonly.d.ts","sourceRoot":"","sources":["../../src/run/rawonly.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;
|
|
1
|
+
{"version":3,"file":"rawonly.d.ts","sourceRoot":"","sources":["../../src/run/rawonly.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAC/D,OAAO,EAAyC,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAE5F,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAEpC;sEACsE;AACtE,eAAO,MAAM,iBAAiB,uGACwE,CAAC;AAEvG;;oEAEoE;AACpE;8DAC8D;AAC9D,MAAM,MAAM,aAAa,GAAG,gBAAgB,GAAG,gBAAgB,CAAC;AAEhE,2EAA2E;AAC3E,eAAO,MAAM,uBAAuB,iHAC4E,CAAC;AAEjH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,GAAE,aAAgC,GAAG,eAAe,CAS9G;AAsBD;;;;;gFAKgF;AAChF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAG3E;AAED;;iDAEiD;AACjD,eAAO,MAAM,6BAA6B,QAIW,CAAC;AAEtD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,GAAG,IAAI,CAQnE;AAED;;;mDAGmD;AACnD,wBAAgB,eAAe,IAAI,YAAY,CAS9C;AAED;;0EAE0E;AAC1E,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,YAAY,CASrF;AAED;;;qCAGqC;AACrC,KAAK,YAAY,GAAG,IAAI,CACtB,GAAG,EACD,KAAK,GACL,WAAW,GACX,WAAW,GACX,iBAAiB,GACjB,QAAQ,GACR,SAAS,GACT,YAAY,CACf,CAAC;AAEF;;;;;;;;;mDASmD;AACnD,wBAAgB,mBAAmB,CAAC,MAAM,GAAE,aAAgC,GAAG,YAAY,CAW1F"}
|
package/dist/run/rawonly.js
CHANGED
|
@@ -19,26 +19,30 @@
|
|
|
19
19
|
* `createRawRig()` factory was considered and REJECTED, spec §2 R1).
|
|
20
20
|
*/
|
|
21
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
exports.RAW_ONLY_FIX_HINT = void 0;
|
|
22
|
+
exports.ABSENT_DEFAULT_ROUTES_WARNING = exports.ABSENT_DEFAULT_FIX_HINT = exports.RAW_ONLY_FIX_HINT = void 0;
|
|
23
23
|
exports.rawOnlyRouteFailure = rawOnlyRouteFailure;
|
|
24
24
|
exports.requireRoutesDir = requireRoutesDir;
|
|
25
|
+
exports.effectiveRoutesDir = effectiveRoutesDir;
|
|
25
26
|
exports.emptyRouteStore = emptyRouteStore;
|
|
26
27
|
exports.loadRoutesOrReport = loadRoutesOrReport;
|
|
27
28
|
exports.rawOnlyRouteMethods = rawOnlyRouteMethods;
|
|
29
|
+
const node_fs_1 = require("node:fs");
|
|
30
|
+
const config_1 = require("../config");
|
|
28
31
|
const loader_1 = require("../routes/loader");
|
|
29
32
|
const load_error_report_1 = require("../telemetry/load-error-report");
|
|
30
33
|
const failures_1 = require("../types/failures");
|
|
31
34
|
/** The teaching remediation for a route-lane call on a runRaw-only rig (spec §2
|
|
32
35
|
* R1). One constant so the failure and the docs stay in lock-step. */
|
|
33
36
|
exports.RAW_ONLY_FIX_HINT = "this rig was constructed runRaw-only (routesDir: null); give createRig a routesDir to serve routes";
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
function rawOnlyRouteFailure(surface) {
|
|
37
|
+
/** The fix hint when the absent-default fallback made the rig raw-only. */
|
|
38
|
+
exports.ABSENT_DEFAULT_FIX_HINT = "create ./modelrig/routes with your route files (`npx modelrig init` drafts them), or set MODELRIG_ROUTES_DIR";
|
|
39
|
+
function rawOnlyRouteFailure(surface, reason = "routesDir-null") {
|
|
38
40
|
return new failures_1.RigFailureError({
|
|
39
41
|
class: "invariant_violation",
|
|
40
|
-
message:
|
|
41
|
-
|
|
42
|
+
message: reason === "absent-default"
|
|
43
|
+
? `${surface} needs routes, but there is no routes directory at ./modelrig/routes, so this rig is raw-only`
|
|
44
|
+
: `${surface} needs routes, but this rig was constructed runRaw-only (routesDir: null)`,
|
|
45
|
+
fixHint: reason === "absent-default" ? exports.ABSENT_DEFAULT_FIX_HINT : exports.RAW_ONLY_FIX_HINT,
|
|
42
46
|
});
|
|
43
47
|
}
|
|
44
48
|
/** A `RunStreamHandle` that fails the runRaw-only way: `result` rejects and the
|
|
@@ -46,8 +50,8 @@ function rawOnlyRouteFailure(surface) {
|
|
|
46
50
|
* throws synchronously (a stream surface always hands back a handle, errors ride
|
|
47
51
|
* `result`/`events`). A synchronous throw here would break the caller's
|
|
48
52
|
* `const h = rig.runStream(...); await h.result` shape. */
|
|
49
|
-
function rawOnlyStreamHandle(surface) {
|
|
50
|
-
const err = rawOnlyRouteFailure(surface);
|
|
53
|
+
function rawOnlyStreamHandle(surface, reason) {
|
|
54
|
+
const err = rawOnlyRouteFailure(surface, reason);
|
|
51
55
|
const result = Promise.reject(err);
|
|
52
56
|
// Keep the rejection handled so an unconsumed handle never becomes an
|
|
53
57
|
// unhandledRejection; the events iterator surfaces the same error.
|
|
@@ -71,6 +75,33 @@ function requireRoutesDir(config, surface) {
|
|
|
71
75
|
throw rawOnlyRouteFailure(surface);
|
|
72
76
|
return config.routesDir;
|
|
73
77
|
}
|
|
78
|
+
/** The warning createRig prints when it falls back to a raw-only rig because the
|
|
79
|
+
* DEFAULT routes directory does not exist (SURF-1, audit #1). One constant so
|
|
80
|
+
* the docs and the test quote the same words. */
|
|
81
|
+
exports.ABSENT_DEFAULT_ROUTES_WARNING = "[modelrig] no routes directory at ./modelrig/routes, so this rig is raw-only: rig.runRaw, " +
|
|
82
|
+
"rig.runRawStream and rig.recordCall work, and rig.run / rig.runBundle need routes. " +
|
|
83
|
+
"Create routes with `npx modelrig init`, set MODELRIG_ROUTES_DIR, or pass `routesDir: null` " +
|
|
84
|
+
"to say raw-only on purpose and silence this line.";
|
|
85
|
+
/**
|
|
86
|
+
* The routes directory createRig actually loads (SURF-1 ruling on audit #1):
|
|
87
|
+
* - `null` stays `null` (CE-8 runRaw-only, silent);
|
|
88
|
+
* - the UNCONFIGURED default (loadConfigFromEnv filled in `./modelrig/routes`;
|
|
89
|
+
* no MODELRIG_ROUTES_DIR, no routesDir override, no `extraRoutesDir`) that
|
|
90
|
+
* does not exist on disk → warn once, return `null`, so
|
|
91
|
+
* a customer who only records calls never crashes on import;
|
|
92
|
+
* - anything explicitly configured is returned as-is, and a missing one still
|
|
93
|
+
* throws the teaching RouteConfigError from the loader.
|
|
94
|
+
*/
|
|
95
|
+
function effectiveRoutesDir(config) {
|
|
96
|
+
const dir = config.routesDir;
|
|
97
|
+
if (dir === null)
|
|
98
|
+
return null;
|
|
99
|
+
if (config.extraRoutesDir === undefined && (0, config_1.isDefaultRoutesDir)(config) && !(0, node_fs_1.existsSync)(dir)) {
|
|
100
|
+
console.warn(exports.ABSENT_DEFAULT_ROUTES_WARNING);
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
return dir;
|
|
104
|
+
}
|
|
74
105
|
/** The empty route store for a runRaw-only rig: `list()` is `[]` (so
|
|
75
106
|
* serveability, capture-detection, and the exported route mirror are all no-ops)
|
|
76
107
|
* and `get()` teaches, though no public surface reaches it — every route method
|
|
@@ -100,21 +131,25 @@ function loadRoutesOrReport(config, routesDir) {
|
|
|
100
131
|
throw err;
|
|
101
132
|
}
|
|
102
133
|
}
|
|
103
|
-
/** The throwing overrides for a runRaw-only rig (spec §2 R1).
|
|
134
|
+
/** The throwing overrides for a runRaw-only rig (spec §2 R1).
|
|
135
|
+
* `bakeoffFromEvalCases` is deliberately NOT overridden (SURF-1, audit #12): it
|
|
136
|
+
* reads eval cases from the local store first, so with none promoted (or none
|
|
137
|
+
* rehydratable) it reports honestly without any routes dir; only a case that
|
|
138
|
+
* actually replays reaches `routes.get`, which teaches the same failure. Each names the
|
|
104
139
|
* surface + the route/task the caller asked for, so the terminal error points at
|
|
105
140
|
* exactly the call that needs a `routesDir`. Promise-returning surfaces REJECT
|
|
106
141
|
* (matching the routed lane, where errors surface as rejections); the two
|
|
107
142
|
* streaming surfaces return a handle synchronously, so they throw synchronously
|
|
108
143
|
* — the caller never reaches an event to await. */
|
|
109
|
-
function rawOnlyRouteMethods() {
|
|
144
|
+
function rawOnlyRouteMethods(reason = "routesDir-null") {
|
|
145
|
+
const fail = (surface) => Promise.reject(rawOnlyRouteFailure(surface, reason));
|
|
110
146
|
return {
|
|
111
|
-
run: (route) =>
|
|
112
|
-
runStream: (route) => rawOnlyStreamHandle(`runStream("${route}")
|
|
113
|
-
runBundle: (bundle) =>
|
|
114
|
-
runBundleStream: (bundle) => rawOnlyStreamHandle(`runBundleStream("${bundle.route}")
|
|
115
|
-
replay: (route) =>
|
|
116
|
-
bakeoff: (route) =>
|
|
117
|
-
|
|
118
|
-
verifySwap: (route) => Promise.reject(rawOnlyRouteFailure(`verifySwap("${route}")`)),
|
|
147
|
+
run: (route) => fail(`run("${route}")`),
|
|
148
|
+
runStream: (route) => rawOnlyStreamHandle(`runStream("${route}")`, reason),
|
|
149
|
+
runBundle: (bundle) => fail(`runBundle("${bundle.route}")`),
|
|
150
|
+
runBundleStream: (bundle) => rawOnlyStreamHandle(`runBundleStream("${bundle.route}")`, reason),
|
|
151
|
+
replay: (route) => fail(`replay("${route}")`),
|
|
152
|
+
bakeoff: (route) => fail(`bakeoff("${route}")`),
|
|
153
|
+
verifySwap: (route) => fail(`verifySwap("${route}")`),
|
|
119
154
|
};
|
|
120
155
|
}
|