@moda-ai/cli 1.30.1 → 1.31.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/README.md +5 -0
- package/dist/{cli-mq00fkwt.js → cli-j3rr0nh7.js} +57 -38
- package/dist/{cli-js4bmw21.js → cli-p3271yp2.js} +232 -50
- package/dist/cli.js +76 -3
- package/dist/{harness-github-actions-v98snak8.js → harness-github-actions-kbw4z7q4.js} +1 -1
- package/dist/{index-14sr487g.js → index-4enh6fq6.js} +72 -7
- package/package.json +1 -1
- package/skills/integration/index.json +15 -3
- package/skills/integration/integration-cloudflare-think/EXAMPLE.md +75 -0
- package/skills/integration/integration-cloudflare-think/SKILL.md +123 -0
- package/skills/integration/integration-node-vercel-ai-sdk/SKILL.md +8 -4
- package/skills/integration/integration-node-vercel-ai-sdk/references/ai7.md +52 -0
- package/skills/integration/integration-node-vercel-ai-sdk/references/prompt-attribution.md +14 -12
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
runPromptsCommand,
|
|
9
9
|
runSkillsCommand,
|
|
10
10
|
runStatusCommand
|
|
11
|
-
} from "./cli-
|
|
11
|
+
} from "./cli-j3rr0nh7.js";
|
|
12
12
|
import {
|
|
13
13
|
ApiError,
|
|
14
14
|
HARNESS_REPORT_APPROVAL_PATH,
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
summarizeHarness,
|
|
30
30
|
terminalStyles,
|
|
31
31
|
validateHarnessReport
|
|
32
|
-
} from "./cli-
|
|
32
|
+
} from "./cli-p3271yp2.js";
|
|
33
33
|
import {
|
|
34
34
|
authFetch,
|
|
35
35
|
clearAuthSession,
|
|
@@ -152,6 +152,29 @@ var ProblemsSchema = z.object({
|
|
|
152
152
|
limit: z.number().min(1).max(25).default(25).optional()
|
|
153
153
|
});
|
|
154
154
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
155
|
+
var ProblemImpactSchema = z.object({
|
|
156
|
+
node_id: z.string().min(1).max(256).optional(),
|
|
157
|
+
problem_id: z.string().regex(UUID_RE).optional(),
|
|
158
|
+
run_id: z.string().min(1).max(256).optional(),
|
|
159
|
+
days_back: z.union([z.literal("all"), z.coerce.number().refine((n) => [1, 3, 7, 30, 90].includes(n), "Use all, 1, 3, 7, 30, or 90 days")]).default(7),
|
|
160
|
+
mode: z.enum(["metrics", "trend", "evidence", "catalog", "overview", "affected-use-cases", "trace-use-cases"]).default("metrics"),
|
|
161
|
+
to: z.string().datetime({ precision: 3 }).optional(),
|
|
162
|
+
cursor: z.string().min(1).optional(),
|
|
163
|
+
conversation_ids: z.string().transform((value, ctx) => {
|
|
164
|
+
try {
|
|
165
|
+
return z.array(z.string().min(1).max(512)).min(1).max(25).parse(JSON.parse(value));
|
|
166
|
+
} catch {
|
|
167
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Use a JSON array of 1–25 conversation IDs" });
|
|
168
|
+
return z.NEVER;
|
|
169
|
+
}
|
|
170
|
+
}).optional()
|
|
171
|
+
}).refine((p) => p.mode !== "trace-use-cases" || p.problem_id && p.conversation_ids, {
|
|
172
|
+
message: "Trace use cases require --problem-id and --conversation-ids"
|
|
173
|
+
}).refine((p) => p.node_id || p.problem_id || ["catalog", "overview"].includes(p.mode), {
|
|
174
|
+
message: "Select --node-id or --problem-id, or use --mode=catalog/overview"
|
|
175
|
+
}).refine((p) => !p.cursor || p.mode === "evidence" && p.to && p.run_id, {
|
|
176
|
+
message: "An evidence cursor requires --mode=evidence, --to, and --run-id from the previous response"
|
|
177
|
+
});
|
|
155
178
|
var PROBLEM_FAMILIES = [
|
|
156
179
|
"tool_failure",
|
|
157
180
|
"emotion",
|
|
@@ -5356,6 +5379,7 @@ Commands:
|
|
|
5356
5379
|
tool-failures Get tool failure overview
|
|
5357
5380
|
tool-failure-detail <tool_name> Get per-tool failure detail
|
|
5358
5381
|
problems Rank cross-signal Problems by root cause (what to fix first)
|
|
5382
|
+
problem-impact Live problem/use-case impact (--node-id, --problem-id, --mode, --days-back)
|
|
5359
5383
|
problem <problem_id> Open one Problem: dossier, or --evidence/--reports/--traces/--feedback
|
|
5360
5384
|
problem-feedback <problem_id> Mark a Problem fixed, dismiss, rename, or flag a bad attribution
|
|
5361
5385
|
step-scores <conversation_id> Graph-PRM step scores (per-segment curves, first bad step, rollup)
|
|
@@ -5514,6 +5538,11 @@ Init flags:
|
|
|
5514
5538
|
--analyst-model=MODEL Model for the claude analyst (default: sonnet;
|
|
5515
5539
|
"default" = the claude CLI's own default;
|
|
5516
5540
|
env: MODA_HARNESS_ANALYST_MODEL)
|
|
5541
|
+
--concern=topology|tools|prompts|evals
|
|
5542
|
+
Harness analyze: restrict the pass to one
|
|
5543
|
+
concern (whole repo, one report scope); an
|
|
5544
|
+
orchestrator merges the per-concern reports
|
|
5545
|
+
(env: MODA_HARNESS_CONCERN, flag wins)
|
|
5517
5546
|
--allow-no-agents Allow harness sync to push a graph with zero
|
|
5518
5547
|
runtime agents despite detected LLM usage
|
|
5519
5548
|
--remote Run harness analysis server-side (Moda-hosted,
|
|
@@ -6216,6 +6245,35 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6216
6245
|
await printWithOptionalWindows(withToolFailureAnchors(data), "examples", params.include_window === true, params.window ?? 1, context.output, warnings);
|
|
6217
6246
|
break;
|
|
6218
6247
|
}
|
|
6248
|
+
case "problem-impact": {
|
|
6249
|
+
const params = ProblemImpactSchema.parse(Object.fromEntries(Object.entries(flags).map(([key, value]) => [key.replace(/-/g, "_"), value])));
|
|
6250
|
+
const query = new URLSearchParams({ days: String(params.days_back), mode: params.mode });
|
|
6251
|
+
for (const [key, value] of Object.entries({
|
|
6252
|
+
nodeId: params.node_id,
|
|
6253
|
+
problemId: params.problem_id,
|
|
6254
|
+
runId: params.run_id,
|
|
6255
|
+
to: params.to,
|
|
6256
|
+
cursor: params.cursor,
|
|
6257
|
+
conversationIds: params.conversation_ids ? JSON.stringify(params.conversation_ids) : undefined
|
|
6258
|
+
}))
|
|
6259
|
+
if (value !== undefined)
|
|
6260
|
+
query.set(key, value);
|
|
6261
|
+
const data = asRecord(await callDataAPI(`/problem-impact?${query}`)) ?? {};
|
|
6262
|
+
const link = new URLSearchParams({ impactDays: String(params.days_back) });
|
|
6263
|
+
const window = asRecord(data.window);
|
|
6264
|
+
const resolvedRun = asString(data.runId);
|
|
6265
|
+
const windowEnd = asString(window?.to);
|
|
6266
|
+
if (resolvedRun)
|
|
6267
|
+
link.set("run", resolvedRun);
|
|
6268
|
+
if (windowEnd)
|
|
6269
|
+
link.set("impactTo", windowEnd);
|
|
6270
|
+
if (params.node_id)
|
|
6271
|
+
link.set("useCase", params.node_id);
|
|
6272
|
+
const problem = asString(data.problemId) || params.problem_id;
|
|
6273
|
+
const path = problem ? `/dashboard/problems/${encodeURIComponent(problem)}` : params.node_id ? `/dashboard/use-cases/${encodeURIComponent(params.node_id)}` : "/dashboard/use-cases";
|
|
6274
|
+
context.output.writeData({ ...data, dashboard_url: `${resolveModaBaseUrl()}${path}?${link}` });
|
|
6275
|
+
break;
|
|
6276
|
+
}
|
|
6219
6277
|
case "problems": {
|
|
6220
6278
|
const args = flagsToArgs(flags);
|
|
6221
6279
|
const params = ProblemsSchema.parse(args);
|
|
@@ -6521,7 +6579,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
6521
6579
|
resetApiRequestCountBeforeRun: true,
|
|
6522
6580
|
telemetry: "result",
|
|
6523
6581
|
handler: async (context) => {
|
|
6524
|
-
const { runInit } = await import("./index-
|
|
6582
|
+
const { runInit } = await import("./index-4enh6fq6.js");
|
|
6525
6583
|
if (context.outputMode === "agent-stream") {
|
|
6526
6584
|
context.output.writeEvent({
|
|
6527
6585
|
event: "started",
|
|
@@ -6903,6 +6961,21 @@ var commandRegistry = createCommandRegistry([
|
|
|
6903
6961
|
examples: ["moda problems", "moda problems --days-back=7 --limit=10"],
|
|
6904
6962
|
...dataApiDefaults
|
|
6905
6963
|
}),
|
|
6964
|
+
legacyCommand({
|
|
6965
|
+
name: "problem-impact",
|
|
6966
|
+
description: "Live problem/use-case impact, full analysis denominators, daily recurrence, and paginated evidence",
|
|
6967
|
+
examples: [
|
|
6968
|
+
"moda problem-impact --mode=catalog",
|
|
6969
|
+
"moda problem-impact --node-id=node_0 --days-back=all",
|
|
6970
|
+
"moda problem-impact --problem-id=<uuid> --mode=metrics",
|
|
6971
|
+
"moda problem-impact --node-id=node_0 --problem-id=<uuid> --mode=evidence",
|
|
6972
|
+
"moda problem-impact --node-id=node_0 --mode=trend --days-back=30",
|
|
6973
|
+
"moda problem-impact --mode=overview --days-back=7",
|
|
6974
|
+
"moda problem-impact --problem-id=<uuid> --mode=affected-use-cases",
|
|
6975
|
+
`moda problem-impact --problem-id=<uuid> --mode=trace-use-cases --conversation-ids='["trace_id"]'`
|
|
6976
|
+
],
|
|
6977
|
+
...dataApiDefaults
|
|
6978
|
+
}),
|
|
6906
6979
|
legacyCommand({
|
|
6907
6980
|
name: "problem",
|
|
6908
6981
|
description: "Open one Problem: dossier, or --evidence/--reports/--traces/--feedback pages (--conversations is a legacy alias of --traces)",
|
|
@@ -5,11 +5,12 @@ import {
|
|
|
5
5
|
detectAgents,
|
|
6
6
|
initPrompts,
|
|
7
7
|
installRules,
|
|
8
|
+
packageAssetPath,
|
|
8
9
|
readSkillsManifest,
|
|
9
10
|
runPromptSync,
|
|
10
11
|
runSkillSync,
|
|
11
12
|
upsertSkillManifestRecord
|
|
12
|
-
} from "./cli-
|
|
13
|
+
} from "./cli-j3rr0nh7.js";
|
|
13
14
|
import {
|
|
14
15
|
codingAgentDisplayName,
|
|
15
16
|
describeCodingAgentEvent,
|
|
@@ -27,7 +28,7 @@ import {
|
|
|
27
28
|
runHarnessCommand,
|
|
28
29
|
startRemoteAnalyze,
|
|
29
30
|
stripAnsi
|
|
30
|
-
} from "./cli-
|
|
31
|
+
} from "./cli-p3271yp2.js";
|
|
31
32
|
import {
|
|
32
33
|
selectTenantAndCreateKey
|
|
33
34
|
} from "./cli-pq6rte0w.js";
|
|
@@ -57,9 +58,7 @@ import { dirname as dirname2, join as join3 } from "node:path";
|
|
|
57
58
|
import { createHash } from "node:crypto";
|
|
58
59
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
59
60
|
import { dirname, join, relative } from "node:path";
|
|
60
|
-
|
|
61
|
-
var PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
62
|
-
var BUNDLED_SKILLS_DIR = join(PACKAGE_ROOT, "skills", "integration");
|
|
61
|
+
var BUNDLED_SKILLS_DIR = packageAssetPath("skills", "integration");
|
|
63
62
|
function bundledSkillsDir() {
|
|
64
63
|
return process.env.MODA_BUNDLED_SKILLS_DIR || BUNDLED_SKILLS_DIR;
|
|
65
64
|
}
|
|
@@ -617,7 +616,7 @@ async function verifyExactTrace(opts) {
|
|
|
617
616
|
const parsed = readAuditCounts(response);
|
|
618
617
|
if (parsed) {
|
|
619
618
|
counts = parsed;
|
|
620
|
-
if (counts.total_spans >= 1 && counts.llm_spans >= 1) {
|
|
619
|
+
if (counts.total_spans >= 1 && counts.llm_spans >= 1 && counts.orphan_count === 0 && !hasUnexpectedAuditDuplicates(response) && completeAI7Projection(response)) {
|
|
621
620
|
return buildEvidence(true);
|
|
622
621
|
}
|
|
623
622
|
}
|
|
@@ -671,6 +670,62 @@ function isRecord2(value) {
|
|
|
671
670
|
function isNonNegativeNumber(value) {
|
|
672
671
|
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
673
672
|
}
|
|
673
|
+
function isAI7Audit(value) {
|
|
674
|
+
return isRecord2(value) && Array.isArray(value.spans) && value.spans.some((span) => isRecord2(span) && isRecord2(span.attributes) && ["cloudflare-think", "vercel-ai7"].includes(String(span.attributes["moda.integration.name"])));
|
|
675
|
+
}
|
|
676
|
+
function hasUnexpectedAuditDuplicates(value) {
|
|
677
|
+
if (!isRecord2(value) || !isRecord2(value.summary))
|
|
678
|
+
return true;
|
|
679
|
+
if (value.summary.duplicate_count === 0 || value.summary.duplicate_count === undefined)
|
|
680
|
+
return false;
|
|
681
|
+
if (!isAI7Audit(value) || !Array.isArray(value.duplicates) || !Array.isArray(value.spans) || value.duplicates.length !== value.summary.duplicate_count)
|
|
682
|
+
return true;
|
|
683
|
+
const spans = value.spans.filter(isRecord2);
|
|
684
|
+
return value.duplicates.some((group) => {
|
|
685
|
+
if (!isRecord2(group) || group.type !== "duplicate_operation" || !Array.isArray(group.span_ids) || group.span_ids.length < 2 || new Set(group.span_ids).size !== group.span_ids.length)
|
|
686
|
+
return true;
|
|
687
|
+
const calls = [];
|
|
688
|
+
for (const id of group.span_ids) {
|
|
689
|
+
const matches = spans.filter((span2) => span2.span_id === id);
|
|
690
|
+
if (matches.length !== 1)
|
|
691
|
+
return true;
|
|
692
|
+
const span = matches[0];
|
|
693
|
+
if (span.type !== "tool" || !isRecord2(span.tool) || typeof span.tool.call_id !== "string" || !span.tool.call_id)
|
|
694
|
+
return true;
|
|
695
|
+
if (!isRecord2(span.attributes) || !["cloudflare-think", "vercel-ai7"].includes(String(span.attributes["moda.integration.name"])))
|
|
696
|
+
return true;
|
|
697
|
+
calls.push(span.tool.call_id);
|
|
698
|
+
}
|
|
699
|
+
return new Set(calls).size !== calls.length;
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
function completeAI7Projection(value) {
|
|
703
|
+
if (!isAI7Audit(value))
|
|
704
|
+
return true;
|
|
705
|
+
if (!isRecord2(value) || !Array.isArray(value.spans) || value.parse_errors !== 0 || !isRecord2(value.summary) || value.summary.truncated !== false)
|
|
706
|
+
return false;
|
|
707
|
+
const spans = value.spans.filter(isRecord2);
|
|
708
|
+
for (const span of spans.filter((span2) => span2.type === "llm")) {
|
|
709
|
+
if (isRecord2(span.attributes) && span.attributes["gen_ai.system_instructions"]) {
|
|
710
|
+
if (!Array.isArray(span.prompt) || !span.prompt.some((message) => isRecord2(message) && message.role === "system"))
|
|
711
|
+
return false;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
const transcript = value.conversationContext;
|
|
715
|
+
if (!isRecord2(transcript) || !isRecord2(transcript.context) || !Array.isArray(transcript.context.messages))
|
|
716
|
+
return false;
|
|
717
|
+
if (transcript.total_messages !== transcript.context.messages.length)
|
|
718
|
+
return false;
|
|
719
|
+
const expected = spans.filter((span) => span.type === "tool").map((span) => isRecord2(span.tool) ? span.tool.call_id : undefined);
|
|
720
|
+
const actual = [];
|
|
721
|
+
for (const message of transcript.context.messages) {
|
|
722
|
+
if (!isRecord2(message) || !Array.isArray(message.tool_calls))
|
|
723
|
+
return false;
|
|
724
|
+
for (const tool of message.tool_calls)
|
|
725
|
+
actual.push(isRecord2(tool) ? tool.id : undefined);
|
|
726
|
+
}
|
|
727
|
+
return expected.every((id) => typeof id === "string") && actual.every((id) => typeof id === "string") && new Set(actual).size === actual.length && expected.length === actual.length && expected.every((id) => actual.includes(id));
|
|
728
|
+
}
|
|
674
729
|
async function fetchAuditWithApiKey(id, apiKey, baseUrl) {
|
|
675
730
|
const url = `${baseUrl}/api/v1/data/audit/${encodeURIComponent(id)}?kind=conversation`;
|
|
676
731
|
const response = await fetch(url, {
|
|
@@ -682,7 +737,17 @@ async function fetchAuditWithApiKey(id, apiKey, baseUrl) {
|
|
|
682
737
|
if (!response.ok) {
|
|
683
738
|
throw new Error(`Audit fetch failed (${response.status}): ${body.slice(0, 200)}`);
|
|
684
739
|
}
|
|
685
|
-
|
|
740
|
+
const audit = JSON.parse(body);
|
|
741
|
+
if (isAI7Audit(audit) && isRecord2(audit)) {
|
|
742
|
+
const contextResponse = await fetch(`${baseUrl}/api/v1/data/conversations/${encodeURIComponent(id)}/context?window=20`, {
|
|
743
|
+
headers: { "x-api-key": apiKey },
|
|
744
|
+
signal: AbortSignal.timeout(1e4)
|
|
745
|
+
});
|
|
746
|
+
if (!contextResponse.ok)
|
|
747
|
+
throw new Error(`AI7 conversation verification failed (${contextResponse.status})`);
|
|
748
|
+
return { ...audit, conversationContext: await contextResponse.json() };
|
|
749
|
+
}
|
|
750
|
+
return audit;
|
|
686
751
|
}
|
|
687
752
|
|
|
688
753
|
// src/init/sdk-integration.ts
|
package/package.json
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": "moda.skill_index.v1",
|
|
3
|
-
"bundled_at": "2026-09-
|
|
4
|
-
"cli_version": "1.
|
|
3
|
+
"bundled_at": "2026-09-15T20:51:36.329Z",
|
|
4
|
+
"cli_version": "1.31.1",
|
|
5
5
|
"skills": [
|
|
6
|
+
{
|
|
7
|
+
"id": "integration-cloudflare-think",
|
|
8
|
+
"version": "1.0.0",
|
|
9
|
+
"summary": "Instrument Think on Workers with AI SDK 7, prompt versions, and acknowledged JSON export.",
|
|
10
|
+
"targets": {
|
|
11
|
+
"language": "node",
|
|
12
|
+
"frameworks": [
|
|
13
|
+
"cloudflare-think"
|
|
14
|
+
],
|
|
15
|
+
"providers": []
|
|
16
|
+
}
|
|
17
|
+
},
|
|
6
18
|
{
|
|
7
19
|
"id": "integration-node-anthropic",
|
|
8
20
|
"version": "1.0.0",
|
|
@@ -51,7 +63,7 @@
|
|
|
51
63
|
},
|
|
52
64
|
{
|
|
53
65
|
"id": "integration-node-vercel-ai-sdk",
|
|
54
|
-
"version": "1.0.
|
|
66
|
+
"version": "1.0.2",
|
|
55
67
|
"summary": "Instrument Vercel AI SDK calls (generateText, streamText, generateObject) with Moda telemetry in a Node.js or TypeScript app.",
|
|
56
68
|
"targets": {
|
|
57
69
|
"language": "node",
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Think with AI SDK 7
|
|
2
|
+
|
|
3
|
+
Merge this hook into the existing Think class, preserving its model, tools,
|
|
4
|
+
routes and hook overrides. Requires SDK 1.16.0's `moda-ai/think` entry point.
|
|
5
|
+
|
|
6
|
+
```typescript
|
|
7
|
+
import { Think, type TurnContext } from '@cloudflare/think';
|
|
8
|
+
import { createThinkTelemetry } from 'moda-ai/think';
|
|
9
|
+
|
|
10
|
+
interface Env { MODA_API_KEY: string; MODA_INGEST_URL?: string; AI: Ai }
|
|
11
|
+
|
|
12
|
+
export class TaskAgent extends Think<Env> {
|
|
13
|
+
getModel() { return '@cf/moonshotai/kimi-k2.7-code' as const; }
|
|
14
|
+
|
|
15
|
+
async beforeTurn(_context: TurnContext) {
|
|
16
|
+
const moda = createThinkTelemetry({
|
|
17
|
+
apiKey: this.env.MODA_API_KEY,
|
|
18
|
+
endpoint: this.env.MODA_INGEST_URL,
|
|
19
|
+
recordInputs: true,
|
|
20
|
+
recordOutputs: true,
|
|
21
|
+
onExportError: error => console.error('Moda export failed', error.message),
|
|
22
|
+
});
|
|
23
|
+
return { telemetry: moda.telemetry };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The helper delegates tracing, export and flushing to the shared AI7 integration;
|
|
29
|
+
non-Think apps use `createAI7Telemetry` from `moda-ai/ai7`. Think's adapter includes
|
|
30
|
+
only its session and turn identity keys from runtime context.
|
|
31
|
+
The helper uses Think's runtime conversation ID by default. To attribute
|
|
32
|
+
managed prompts, import the synced `.moda/prompts.lock.json` at build time
|
|
33
|
+
and add `prompt: lock.prompts['your.prompt.key']` to the options. Keep the
|
|
34
|
+
app's own prompt rendering. The helper flushes on completion/abort and
|
|
35
|
+
retains failed batches for an explicit retry while the object is alive.
|
|
36
|
+
|
|
37
|
+
## Smoke through an existing callable
|
|
38
|
+
|
|
39
|
+
For the upstream `think-submissions` app, the public API is `submitTask` and
|
|
40
|
+
`inspectTask`. Adapt these names to the application's existing callables.
|
|
41
|
+
Use a temporary script, with the CLI's verification ID as its argument:
|
|
42
|
+
|
|
43
|
+
```javascript
|
|
44
|
+
import { AgentClient } from 'agents/client';
|
|
45
|
+
|
|
46
|
+
const id = process.argv[2];
|
|
47
|
+
if (!id) throw new Error('Pass the CLI verification ID');
|
|
48
|
+
const client = new AgentClient({
|
|
49
|
+
host: '127.0.0.1:4190', protocol: 'ws', agent: 'TaskAgent', name: id,
|
|
50
|
+
});
|
|
51
|
+
try {
|
|
52
|
+
const task = await client.call('submitTask', ['Reply with OK.', crypto.randomUUID()]);
|
|
53
|
+
let completed = false;
|
|
54
|
+
for (let attempt = 0; attempt < 60; attempt++) {
|
|
55
|
+
const status = await client.call('inspectTask', [task.submissionId]);
|
|
56
|
+
if (status.status === 'completed') { completed = true; break; }
|
|
57
|
+
if (['error', 'aborted', 'skipped'].includes(status.status)) {
|
|
58
|
+
throw new Error(`Submission failed: ${status.status}`);
|
|
59
|
+
}
|
|
60
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
61
|
+
}
|
|
62
|
+
if (!completed) throw new Error('Submission timed out');
|
|
63
|
+
} finally {
|
|
64
|
+
client.close();
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
This example has one conversation per named Agent: set the telemetry option
|
|
69
|
+
`conversationId: this.name` so the smoke's Agent name becomes the verification
|
|
70
|
+
ID. Keep multi-session apps' own session identity instead. Never hard-code the
|
|
71
|
+
verification ID in the application's source. Confirm delivery with the CLI's
|
|
72
|
+
trace check after the submission completes.
|
|
73
|
+
Use a new submission idempotency key on each smoke attempt, while keeping the
|
|
74
|
+
same verification conversation ID. Reusing a completed submission's key only
|
|
75
|
+
returns its old acknowledgment and does not exercise repaired instrumentation.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: integration-cloudflare-think
|
|
3
|
+
name: "Integrate Moda with Cloudflare Think"
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
category: integration
|
|
6
|
+
targets:
|
|
7
|
+
language: node
|
|
8
|
+
frameworks:
|
|
9
|
+
- cloudflare-think
|
|
10
|
+
providers: []
|
|
11
|
+
sdk_package: moda-ai
|
|
12
|
+
summary: "Instrument Think on Workers with AI SDK 7, prompt versions, and acknowledged JSON export."
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Integrate Moda with Cloudflare Think
|
|
16
|
+
|
|
17
|
+
## Safety rules (apply to every step)
|
|
18
|
+
|
|
19
|
+
- Only touch the repository you were launched in.
|
|
20
|
+
- Never write the `MODA_API_KEY` value (or any secret) into source files, and
|
|
21
|
+
never create or edit `.env` files. The key belongs in the runtime
|
|
22
|
+
environment or a secret manager.
|
|
23
|
+
- Make the smallest correct change. Match the existing code style and imports.
|
|
24
|
+
- Do not commit unless the user asks.
|
|
25
|
+
- If a step is ambiguous in this repository, stop and report rather than
|
|
26
|
+
guessing.
|
|
27
|
+
|
|
28
|
+
## 1. Find the real Think entry point
|
|
29
|
+
|
|
30
|
+
Find classes extending `Think` from `@cloudflare/think`, their `beforeTurn`
|
|
31
|
+
hooks, session identity, and submission/chat route. Read installed versions.
|
|
32
|
+
This recipe supports Think 0.18, AI SDK 7 and `@ai-sdk/otel` 1.x. For a
|
|
33
|
+
different major, report the compatibility gap instead of upgrading the app.
|
|
34
|
+
|
|
35
|
+
## 2. Install the Workers integration
|
|
36
|
+
|
|
37
|
+
With the repository's package manager, install `moda-ai` with the
|
|
38
|
+
`moda-ai/think` export (1.16.0 or later) and `@ai-sdk/otel` 1.x. Confirm the
|
|
39
|
+
export exists before editing call sites. If it is not published yet, report
|
|
40
|
+
the release prerequisite. Retain Workers' `nodejs_compat` configuration.
|
|
41
|
+
|
|
42
|
+
The dedicated entry point owns a per-turn tracer and fetch-based JSON
|
|
43
|
+
exporter. Instrument the loop once: reconcile existing instrumentation or
|
|
44
|
+
native export to Moda before adding a second telemetry path.
|
|
45
|
+
|
|
46
|
+
## 3. Wire the existing hook
|
|
47
|
+
|
|
48
|
+
Follow [EXAMPLE.md](EXAMPLE.md). Create telemetry per invocation and merge
|
|
49
|
+
it with existing hook return values. Preserve unrelated integrations.
|
|
50
|
+
Supply `env.MODA_API_KEY` from runtime secrets.
|
|
51
|
+
Pass `env.MODA_INGEST_URL` as `endpoint` when an ingest override is provided
|
|
52
|
+
(for example, an isolated test deployment). Make both values available in
|
|
53
|
+
the Worker runtime; shell variables alone are not automatically Worker bindings.
|
|
54
|
+
Use the app's session ID or
|
|
55
|
+
let the helper use Think's runtime conversation ID; agent name is suitable
|
|
56
|
+
only for an agent with one conversation.
|
|
57
|
+
|
|
58
|
+
Message and tool payload capture is opt-in via `recordInputs` and
|
|
59
|
+
`recordOutputs`. The helper flushes on completion/abort. Keep its `flush()`
|
|
60
|
+
available for explicit delivery verification and report `onExportError`.
|
|
61
|
+
Retries preserve span IDs. Pending exports live in memory, so abrupt
|
|
62
|
+
runtime termination does not have durable archival guarantees.
|
|
63
|
+
|
|
64
|
+
## 4. Ingest reusable prompts when requested
|
|
65
|
+
|
|
66
|
+
Find the source prompt (`getSystemPrompt` or context configuration). Inline
|
|
67
|
+
TypeScript is not discovered by `moda prompts`. Extract the reusable
|
|
68
|
+
definition into a supported `.prompt.json` file and have the original hook
|
|
69
|
+
read it. Run `moda prompts init` then `moda prompts sync`; require the
|
|
70
|
+
expected definition to be present. Import `.moda/prompts.lock.json` at
|
|
71
|
+
build time and pass its `{ key, promptId, versionId }` entry as `prompt`.
|
|
72
|
+
Re-sync changed definitions before building.
|
|
73
|
+
|
|
74
|
+
Use the CLI's actual JSON fields, for example:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"key": "task-agent.system",
|
|
79
|
+
"name": "Task agent system prompt",
|
|
80
|
+
"systemPrompt": "The exact existing getSystemPrompt text goes here."
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Existing JSON definitions with a plain-text `template` are also accepted;
|
|
85
|
+
the CLI maps that text to registered content without rewriting the file.
|
|
86
|
+
Keep the application's existing prompt field and have `getSystemPrompt` read
|
|
87
|
+
that same definition so runtime and registry cannot drift.
|
|
88
|
+
Check the sync payload includes the actual text; a lock entry or
|
|
89
|
+
version ID alone does not prove that a nonempty definition was registered.
|
|
90
|
+
|
|
91
|
+
Keep memory and variable substitutions separate from reusable template
|
|
92
|
+
versions. Think adds instructions/tools: verify actual system instructions
|
|
93
|
+
on each model span as well as the registry definition.
|
|
94
|
+
Reference: https://docs.moda.dev/prompt-management/attribution
|
|
95
|
+
|
|
96
|
+
## 5. Validate through the original application route
|
|
97
|
+
|
|
98
|
+
Build and typecheck. Exercise a fresh session through the original route:
|
|
99
|
+
an exact response, a real tool invocation, and a second turn. Wait for
|
|
100
|
+
export acknowledgment. Use `moda audit` on that exact conversation to check
|
|
101
|
+
expected model/tool counts, system/user inputs, output, prompt version,
|
|
102
|
+
and zero duplicates/orphans. Check conversation records too: step spans
|
|
103
|
+
must not become tools, real call IDs must occur once, and successful tools
|
|
104
|
+
must not report failures. Re-running onboarding must reuse the integration.
|
|
105
|
+
|
|
106
|
+
For callable submission methods, use the installed `AgentClient` from
|
|
107
|
+
`agents/client` (the same protocol as `useAgent().call()`), rather than
|
|
108
|
+
reimplementing its wire protocol. A plain HTTP GET to a WebSocket route may
|
|
109
|
+
return 404 even when the WebSocket works. A submission acknowledgment only
|
|
110
|
+
means queued: poll the app's inspection method until completion before
|
|
111
|
+
checking telemetry.
|
|
112
|
+
|
|
113
|
+
The CLI supplies an exact verification conversation ID. In a single-conversation
|
|
114
|
+
per-Agent app such as `think-submissions`, `conversationId: this.name` lets a
|
|
115
|
+
fresh named Agent use that ID. For apps with multiple sessions per Agent, use
|
|
116
|
+
their actual session ID instead. Never hard-code the verification ID in source.
|
|
117
|
+
|
|
118
|
+
Report missing evidence explicitly; connectivity alone is not a complete
|
|
119
|
+
integration verdict. Finish with:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
moda doctor --online --json
|
|
123
|
+
```
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
id: integration-node-vercel-ai-sdk
|
|
3
3
|
name: "Integrate Moda with the Vercel AI SDK (Node.js / TypeScript)"
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.2
|
|
5
5
|
category: integration
|
|
6
6
|
targets:
|
|
7
7
|
language: node
|
|
@@ -37,6 +37,11 @@ prefer those; they match the installed SDK version).
|
|
|
37
37
|
Find the real LLM call sites: search for `generateText`, `streamText`,
|
|
38
38
|
`generateObject`, or `streamObject` imported from the `ai` package.
|
|
39
39
|
|
|
40
|
+
Check the installed `ai` major version before selecting an API. For
|
|
41
|
+
`@cloudflare/think` with AI SDK 7, use `integration-cloudflare-think`.
|
|
42
|
+
For other AI SDK 7 apps, follow `references/ai7.md` instead of Steps 2 onward
|
|
43
|
+
and the legacy `EXAMPLE.md`. The remaining steps here target AI SDK through v6.
|
|
44
|
+
|
|
40
45
|
- If you find none, stop and report: this skill only applies to apps that
|
|
41
46
|
call the Vercel AI SDK directly.
|
|
42
47
|
- Instrument the Vercel AI SDK layer only. Do NOT also instrument raw
|
|
@@ -140,9 +145,8 @@ identifier exists anywhere, stop and report; do not invent one.
|
|
|
140
145
|
|
|
141
146
|
## Step 6: Attribute managed prompts (only if the repo uses them)
|
|
142
147
|
|
|
143
|
-
Only if this repository has a `.moda/prompts.yml` manifest:
|
|
144
|
-
|
|
145
|
-
at exact prompt versions. See
|
|
148
|
+
Only if this repository has a `.moda/prompts.yml` manifest: read synced prompt
|
|
149
|
+
IDs from the lockfile and attach them explicitly to telemetry metadata. See
|
|
146
150
|
[references/prompt-attribution.md](references/prompt-attribution.md).
|
|
147
151
|
If there is no prompt manifest, skip this step.
|
|
148
152
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# AI SDK 7: shared integration
|
|
2
|
+
|
|
3
|
+
Apply the parent skill's safety rules. This recipe supports direct
|
|
4
|
+
`generateText` and `streamText` calls on AI SDK 7, in Node or Workers.
|
|
5
|
+
Think apps use the `integration-cloudflare-think` entry because their framework
|
|
6
|
+
supplies session/turn identity.
|
|
7
|
+
|
|
8
|
+
1. Confirm the installed `ai` major is 7. Install `moda-ai` >=1.16.0 and
|
|
9
|
+
`@ai-sdk/otel` 1.x using the repository's package manager. Verify
|
|
10
|
+
`moda-ai/ai7` exports `createAI7Telemetry`. If unavailable, stop and report
|
|
11
|
+
that the SDK release is required; do not substitute the older tracer helper.
|
|
12
|
+
2. Keep the app's model, tools, routes, prompts and streaming behavior. Create
|
|
13
|
+
the helper using the provisioned key from the runtime environment. Set
|
|
14
|
+
`conversationId` to the existing app session ID; without it each AI7 call is
|
|
15
|
+
a separate conversation. Set `userId` when the app has a user identifier.
|
|
16
|
+
3. Set the existing call's `telemetry` to the returned configuration. Preserve
|
|
17
|
+
existing integrations when merging: retain their order and append Moda's
|
|
18
|
+
integration array. Avoid also tracing the underlying provider client.
|
|
19
|
+
4. Prompt/tool payload capture defaults off. Enable `recordInputs` and
|
|
20
|
+
`recordOutputs` when prompt/content ingestion is required and authorized.
|
|
21
|
+
After source prompt extraction and `moda prompts sync`, pass the matching
|
|
22
|
+
lockfile `{key, promptId, versionId}` as `prompt`; keep the app's rendering.
|
|
23
|
+
5. Await `flush()` after a completed generation (or a fully consumed stream)
|
|
24
|
+
for onboarding verification. Supply `onExportError` for runtime visibility;
|
|
25
|
+
AI7 may swallow hook exceptions. In Workers retain `nodejs_compat` and use
|
|
26
|
+
the runtime's request/stream lifecycle to finish asynchronous work.
|
|
27
|
+
|
|
28
|
+
For same-zone Worker-to-Worker routing on `workers.dev`, configure a service
|
|
29
|
+
binding and pass `fetch: (url, init) => env.MODA_INGEST.fetch(new Request(url, init))`.
|
|
30
|
+
Other deployments use the default native fetch. This changes transport only;
|
|
31
|
+
the SDK still serializes, authenticates, retries and verifies the export response.
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { createAI7Telemetry } from 'moda-ai/ai7';
|
|
35
|
+
|
|
36
|
+
const moda = createAI7Telemetry({
|
|
37
|
+
apiKey: process.env.MODA_API_KEY!, // Workers: use the request's env binding.
|
|
38
|
+
conversationId: sessionId,
|
|
39
|
+
recordInputs: true,
|
|
40
|
+
recordOutputs: true,
|
|
41
|
+
onExportError: error => console.error('Moda export failed', error.message),
|
|
42
|
+
});
|
|
43
|
+
const result = await generateText({ ...existingOptions, telemetry: moda.telemetry });
|
|
44
|
+
await moda.flush();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Validate using a fresh verification conversation ID and the provisioned key.
|
|
48
|
+
Require complete spans, normalized system prompts when captured, actual tool
|
|
49
|
+
calls/results without duplicate summary records, and a complete conversation
|
|
50
|
+
window. The CLI verifier checks this for `vercel-ai7` and `cloudflare-think`.
|
|
51
|
+
An HTTP 200 alone is not acceptance. Retries retain span IDs, but the shared
|
|
52
|
+
export buffer is in memory and cannot guarantee delivery across isolate eviction.
|
|
@@ -3,29 +3,31 @@
|
|
|
3
3
|
Only relevant when the repository manages prompts with Moda
|
|
4
4
|
(`.moda/prompts.yml` exists and prompts are synced with `moda prompts sync`).
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
`moda
|
|
10
|
-
traces point at the exact prompt version:
|
|
6
|
+
Run `moda prompts sync`, read `.moda/prompts.lock.json`, and pass its registry
|
|
7
|
+
IDs explicitly in telemetry metadata. Keep the application's own rendering.
|
|
8
|
+
This example uses the tracer-based AI SDK API through v6. Think with AI SDK
|
|
9
|
+
7 uses `moda-ai/think` and `telemetry.integrations`; see its catalog entry.
|
|
11
10
|
|
|
12
11
|
```typescript
|
|
13
12
|
import { Moda } from 'moda-ai';
|
|
14
13
|
import { generateText } from 'ai';
|
|
15
14
|
import { openai } from '@ai-sdk/openai';
|
|
15
|
+
import prompts from './.moda/prompts.lock.json';
|
|
16
16
|
|
|
17
|
-
const
|
|
18
|
-
ticket: { text: userMessage },
|
|
19
|
-
});
|
|
17
|
+
const version = prompts.prompts['support.triage'];
|
|
20
18
|
|
|
21
19
|
const result = await generateText({
|
|
22
20
|
model: openai('gpt-4o'),
|
|
23
|
-
|
|
24
|
-
experimental_telemetry: Moda.getVercelAITelemetry(
|
|
21
|
+
prompt: 'Triage this support request.',
|
|
22
|
+
experimental_telemetry: Moda.getVercelAITelemetry({ metadata: {
|
|
23
|
+
'moda.prompt_key': version.key,
|
|
24
|
+
'moda.prompt_id': version.promptId,
|
|
25
|
+
'moda.prompt_version_id': version.versionId,
|
|
26
|
+
}}),
|
|
25
27
|
});
|
|
26
28
|
```
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
Re-sync a changed definition before building. Different variable values can
|
|
31
|
+
share a template version; full model inputs are recorded separately.
|
|
30
32
|
|
|
31
33
|
Full reference: https://docs.moda.dev/ingestion/vercel-ai-sdk
|