@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/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  runPromptsCommand,
9
9
  runSkillsCommand,
10
10
  runStatusCommand
11
- } from "./cli-mq00fkwt.js";
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-js4bmw21.js";
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-14sr487g.js");
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)",
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  buildSourceSnapshot
3
- } from "./cli-js4bmw21.js";
3
+ } from "./cli-p3271yp2.js";
4
4
  import"./cli-59yacef3.js";
5
5
 
6
6
  // src/harness-github-actions.ts
@@ -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-mq00fkwt.js";
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-js4bmw21.js";
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
- import { fileURLToPath } from "node:url";
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
- return JSON.parse(body);
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@moda-ai/cli",
3
- "version": "1.30.1",
3
+ "version": "1.31.1",
4
4
  "description": "CLI for Moda - AI agent analytics and observability",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,8 +1,20 @@
1
1
  {
2
2
  "schema_version": "moda.skill_index.v1",
3
- "bundled_at": "2026-09-02T04:30:52.582Z",
4
- "cli_version": "1.30.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.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.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: render managed
144
- prompts through the SDK before creating the telemetry config, so traces point
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
- Render the managed prompt through the SDK, then create the telemetry config.
7
- `Moda.getVercelAITelemetry()` includes the rendered prompt's metadata
8
- (`moda.prompt_key`, `moda.prompt_id`, `moda.prompt_version`,
9
- `moda.prompt_version_id`) in `experimental_telemetry.metadata`, so production
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 rendered = Moda.prompt('support.triage').render({
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
- messages: rendered.messages,
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
- Order matters: render the prompt before calling
29
- `Moda.getVercelAITelemetry()` so prompt metadata is attached.
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