forgeos 0.1.0-alpha.2 → 0.1.0-alpha.4

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.
Files changed (180) hide show
  1. package/AGENTS.md +38 -3
  2. package/CHANGELOG.md +29 -0
  3. package/README.md +25 -10
  4. package/package.json +8 -5
  5. package/src/forge/_generated/actionSubscriptions.json +2 -2
  6. package/src/forge/_generated/actionSubscriptions.ts +3 -3
  7. package/src/forge/_generated/agentAdapterManifest.json +2 -2
  8. package/src/forge/_generated/agentAdapterManifest.ts +3 -3
  9. package/src/forge/_generated/agentContract.json +2 -2
  10. package/src/forge/_generated/agentContract.ts +183 -50
  11. package/src/forge/_generated/agentQuickstart.md +3 -1
  12. package/src/forge/_generated/agentTools.json +2 -0
  13. package/src/forge/_generated/agentTools.md +16 -0
  14. package/src/forge/_generated/agentTools.ts +12 -0
  15. package/src/forge/_generated/aiContext.ts +67 -1
  16. package/src/forge/_generated/aiModels.json +2 -2
  17. package/src/forge/_generated/aiModels.ts +17 -1
  18. package/src/forge/_generated/aiProviders.json +1 -1
  19. package/src/forge/_generated/aiProviders.ts +1 -1
  20. package/src/forge/_generated/aiRegistry.json +2 -2
  21. package/src/forge/_generated/aiRegistry.ts +7 -5
  22. package/src/forge/_generated/api.json +2 -2
  23. package/src/forge/_generated/api.ts +1 -1
  24. package/src/forge/_generated/appGraph.json +2 -2
  25. package/src/forge/_generated/appGraph.ts +512 -260
  26. package/src/forge/_generated/appMap.md +21 -1
  27. package/src/forge/_generated/artifactManifest.json +2 -2
  28. package/src/forge/_generated/artifactManifest.ts +2 -2
  29. package/src/forge/_generated/authClaims.json +1 -1
  30. package/src/forge/_generated/authClaims.ts +1 -1
  31. package/src/forge/_generated/authConfig.json +1 -1
  32. package/src/forge/_generated/authConfig.ts +1 -1
  33. package/src/forge/_generated/authContext.ts +1 -1
  34. package/src/forge/_generated/authRegistry.json +1 -1
  35. package/src/forge/_generated/authRegistry.ts +1 -1
  36. package/src/forge/_generated/buildInfo.json +2 -2
  37. package/src/forge/_generated/buildInfo.ts +4 -4
  38. package/src/forge/_generated/capabilityMap.json +2 -2
  39. package/src/forge/_generated/capabilityMap.md +1 -1
  40. package/src/forge/_generated/capabilityMap.ts +2 -2
  41. package/src/forge/_generated/client.ts +1 -1
  42. package/src/forge/_generated/clientApi.ts +1 -1
  43. package/src/forge/_generated/clientManifest.json +2 -2
  44. package/src/forge/_generated/clientManifest.ts +3 -3
  45. package/src/forge/_generated/clientTypes.ts +1 -1
  46. package/src/forge/_generated/configRegistry.json +1 -1
  47. package/src/forge/_generated/configRegistry.ts +1 -1
  48. package/src/forge/_generated/dataGraph.json +2 -2
  49. package/src/forge/_generated/dataGraph.ts +3 -3
  50. package/src/forge/_generated/db.json +1 -1
  51. package/src/forge/_generated/db.ts +1 -1
  52. package/src/forge/_generated/dbSecurityManifest.json +1 -1
  53. package/src/forge/_generated/dbSecurityManifest.ts +1 -1
  54. package/src/forge/_generated/dbSessionContext.json +1 -1
  55. package/src/forge/_generated/dbSessionContext.ts +1 -1
  56. package/src/forge/_generated/deployManifest.json +2 -2
  57. package/src/forge/_generated/deployManifest.ts +7 -7
  58. package/src/forge/_generated/devManifest.json +2 -2
  59. package/src/forge/_generated/devManifest.ts +18 -3
  60. package/src/forge/_generated/envSchema.json +1 -1
  61. package/src/forge/_generated/envSchema.ts +1 -1
  62. package/src/forge/_generated/frontendGraph.json +1 -1
  63. package/src/forge/_generated/frontendGraph.ts +1 -1
  64. package/src/forge/_generated/importGuards.json +1 -1
  65. package/src/forge/_generated/importGuards.ts +1 -1
  66. package/src/forge/_generated/index.ts +2 -1
  67. package/src/forge/_generated/liveProductionManifest.json +1 -1
  68. package/src/forge/_generated/liveProductionManifest.ts +1 -1
  69. package/src/forge/_generated/liveProtocol.json +1 -1
  70. package/src/forge/_generated/liveProtocol.ts +1 -1
  71. package/src/forge/_generated/liveQueryRegistry.json +2 -2
  72. package/src/forge/_generated/liveQueryRegistry.ts +3 -3
  73. package/src/forge/_generated/liveTransportConfig.json +1 -1
  74. package/src/forge/_generated/liveTransportConfig.ts +1 -1
  75. package/src/forge/_generated/makeRegistry.json +2 -2
  76. package/src/forge/_generated/makeRegistry.ts +16 -2
  77. package/src/forge/_generated/makeTemplates.json +2 -2
  78. package/src/forge/_generated/makeTemplates.ts +6 -1
  79. package/src/forge/_generated/mockMap.json +1 -1
  80. package/src/forge/_generated/mockMap.ts +1 -1
  81. package/src/forge/_generated/operationPlaybooks.md +34 -14
  82. package/src/forge/_generated/packageGraph.json +2 -2
  83. package/src/forge/_generated/packageGraph.ts +8808 -4723
  84. package/src/forge/_generated/packageUpgradeRegistry.json +2 -2
  85. package/src/forge/_generated/packageUpgradeRegistry.ts +2 -2
  86. package/src/forge/_generated/permissionMatrix.json +2 -2
  87. package/src/forge/_generated/permissionMatrix.ts +3 -3
  88. package/src/forge/_generated/policyRegistry.json +2 -2
  89. package/src/forge/_generated/policyRegistry.ts +3 -3
  90. package/src/forge/_generated/queryRegistry.json +2 -2
  91. package/src/forge/_generated/queryRegistry.ts +3 -3
  92. package/src/forge/_generated/react.d.ts +1 -1
  93. package/src/forge/_generated/react.ts +1 -1
  94. package/src/forge/_generated/reactManifest.json +2 -2
  95. package/src/forge/_generated/reactManifest.ts +3 -3
  96. package/src/forge/_generated/releaseManifest.json +2 -2
  97. package/src/forge/_generated/releaseManifest.ts +3 -3
  98. package/src/forge/_generated/rlsPolicies.json +1 -1
  99. package/src/forge/_generated/rlsPolicies.sql +1 -1
  100. package/src/forge/_generated/rlsPolicies.ts +1 -1
  101. package/src/forge/_generated/runtimeGraph.json +2 -2
  102. package/src/forge/_generated/runtimeGraph.ts +3 -3
  103. package/src/forge/_generated/runtimeMatrix.json +2 -2
  104. package/src/forge/_generated/runtimeMatrix.ts +8684 -1939
  105. package/src/forge/_generated/runtimeRegistry.ts +1 -1
  106. package/src/forge/_generated/runtimeRules.md +13 -1
  107. package/src/forge/_generated/secretRegistry.json +1 -1
  108. package/src/forge/_generated/secretRegistry.ts +1 -1
  109. package/src/forge/_generated/secretsContext.ts +1 -1
  110. package/src/forge/_generated/serverApi.ts +1 -1
  111. package/src/forge/_generated/sourceMapManifest.json +2 -2
  112. package/src/forge/_generated/sourceMapManifest.ts +2 -2
  113. package/src/forge/_generated/sqlPlan.json +1 -1
  114. package/src/forge/_generated/sqlPlan.ts +1 -1
  115. package/src/forge/_generated/subscriptionManifest.json +2 -2
  116. package/src/forge/_generated/subscriptionManifest.ts +3 -3
  117. package/src/forge/_generated/symbolicationManifest.json +2 -2
  118. package/src/forge/_generated/symbolicationManifest.ts +2 -2
  119. package/src/forge/_generated/telemetryRegistry.json +2 -2
  120. package/src/forge/_generated/telemetryRegistry.ts +3 -3
  121. package/src/forge/_generated/telemetrySinks.json +2 -2
  122. package/src/forge/_generated/telemetrySinks.ts +2 -2
  123. package/src/forge/_generated/tenantScope.json +2 -2
  124. package/src/forge/_generated/tenantScope.ts +3 -3
  125. package/src/forge/_generated/testGraph.json +2 -2
  126. package/src/forge/_generated/testGraph.ts +339 -17
  127. package/src/forge/_generated/testPlanRegistry.json +2 -2
  128. package/src/forge/_generated/testPlanRegistry.ts +2 -2
  129. package/src/forge/_generated/uiRoutes.json +1 -1
  130. package/src/forge/_generated/uiRoutes.ts +1 -1
  131. package/src/forge/_generated/uiScenarios.json +1 -1
  132. package/src/forge/_generated/uiScenarios.ts +1 -1
  133. package/src/forge/_generated/uiTestManifest.json +2 -2
  134. package/src/forge/_generated/uiTestManifest.ts +2 -2
  135. package/src/forge/_generated/workflowRegistry.json +2 -2
  136. package/src/forge/_generated/workflowRegistry.ts +3 -3
  137. package/src/forge/_generated/workflowSubscriptions.json +2 -2
  138. package/src/forge/_generated/workflowSubscriptions.ts +3 -3
  139. package/src/forge/cli/ai.ts +351 -1
  140. package/src/forge/cli/auth.ts +36 -1
  141. package/src/forge/cli/commands.ts +19 -0
  142. package/src/forge/cli/parse.ts +67 -8
  143. package/src/forge/cli/rls.ts +529 -17
  144. package/src/forge/cli/secrets.ts +46 -1
  145. package/src/forge/cli/security.ts +269 -0
  146. package/src/forge/compiler/agent-contract/build.ts +289 -8
  147. package/src/forge/compiler/agent-contract/types.ts +43 -0
  148. package/src/forge/compiler/ai-registry/build.ts +62 -1
  149. package/src/forge/compiler/ai-registry/constants.ts +1 -1
  150. package/src/forge/compiler/ai-registry/parse.ts +98 -4
  151. package/src/forge/compiler/app-graph/forge-apis.ts +1 -0
  152. package/src/forge/compiler/dev-manifest/build.ts +3 -0
  153. package/src/forge/compiler/diagnostics/codes.ts +15 -0
  154. package/src/forge/compiler/diagnostics/create.ts +1 -1
  155. package/src/forge/compiler/make-registry/build.ts +13 -0
  156. package/src/forge/compiler/orchestrator/plan.ts +11 -0
  157. package/src/forge/compiler/orchestrator/serialize.ts +68 -0
  158. package/src/forge/compiler/package-graph/compiler.ts +13 -3
  159. package/src/forge/compiler/types/ai-registry.ts +25 -1
  160. package/src/forge/compiler/types/app-graph.ts +1 -0
  161. package/src/forge/compiler/types/cli.ts +1 -0
  162. package/src/forge/compiler/types/dev-manifest.ts +3 -0
  163. package/src/forge/dev/server.ts +508 -1
  164. package/src/forge/make/index.ts +126 -3
  165. package/src/forge/make/templates.ts +188 -0
  166. package/src/forge/make/types.ts +1 -0
  167. package/src/forge/runtime/ai/context.ts +210 -5
  168. package/src/forge/runtime/ai/types.ts +70 -0
  169. package/src/forge/runtime/auth/claims.ts +32 -0
  170. package/src/forge/runtime/auth/errors.ts +2 -0
  171. package/src/forge/runtime/context/create-context.ts +30 -6
  172. package/src/forge/runtime/db/memory-adapter.ts +2 -2
  173. package/src/forge/runtime/telemetry/scrubber.ts +56 -5
  174. package/src/forge/runtime/webhooks/security.ts +184 -0
  175. package/src/forge/server.ts +93 -0
  176. package/src/forge/version.ts +1 -1
  177. package/templates/b2b-support-web/package.json +1 -0
  178. package/templates/b2b-support-web/tsconfig.json +4 -1
  179. package/templates/minimal-web/package.json +1 -0
  180. package/templates/minimal-web/tsconfig.json +3 -1
@@ -10,6 +10,9 @@ import { parseFieldSpec, parseFields, splitTopLevel } from "./fields.ts";
10
10
  import { camelCase, kebabCase, pascalCase, singularize, titleCase } from "./naming.ts";
11
11
  import {
12
12
  renderAction,
13
+ renderAiAgentFile,
14
+ renderAiChatComponent,
15
+ renderAiChatPage,
13
16
  renderCreateCommand,
14
17
  renderCreateForm,
15
18
  renderDeleteCommand,
@@ -17,6 +20,7 @@ import {
17
20
  renderListComponent,
18
21
  renderListQuery,
19
22
  renderLiveQuery,
23
+ renderNextAiPackage,
20
24
  renderPage,
21
25
  renderPlaceholderTest,
22
26
  renderPolicyFile,
@@ -57,6 +61,7 @@ export const MAKE_PRIMITIVES: MakePrimitive[] = [
57
61
  "component",
58
62
  "page",
59
63
  "ui",
64
+ "ai-chat",
60
65
  "resource",
61
66
  "apply",
62
67
  "rollback",
@@ -103,6 +108,8 @@ const EXPLANATIONS: Record<MakeIntent["kind"], string> = {
103
108
  "Adds a minimal app page under web/app/<route>/page.tsx.",
104
109
  ui:
105
110
  "Adds a minimal Vite React web app with ForgeProvider devAuth and a generated client bridge.",
111
+ "ai-chat":
112
+ "Adds a Forge AI agent definition and a React chat component that calls the Forge /ai/agents/run runtime endpoint.",
106
113
  resource:
107
114
  "Creates a full resource slice: table, policies, CRUD commands, queries, liveQuery, action, optional React, and tests.",
108
115
  };
@@ -521,7 +528,7 @@ function buildIntent(options: MakeCommandOptions): {
521
528
  );
522
529
  }
523
530
  const name = options.name ?? (kind === "field" ? "" : undefined);
524
- if (!name && !["component", "page", "ui"].includes(kind)) {
531
+ if (!name && !["component", "page", "ui", "ai-chat"].includes(kind)) {
525
532
  diagnostics.push(
526
533
  diagnostic("error", "FORGE_MAKE_PATCH_UNSAFE", `forge make ${kind} requires a name`),
527
534
  );
@@ -555,7 +562,8 @@ function buildIntent(options: MakeCommandOptions): {
555
562
  kind === "resource" ||
556
563
  kind === "component" ||
557
564
  kind === "page" ||
558
- kind === "ui",
565
+ kind === "ui" ||
566
+ kind === "ai-chat",
559
567
  tests: options.withTests || kind === "resource",
560
568
  policy:
561
569
  options.policy ??
@@ -566,7 +574,7 @@ function buildIntent(options: MakeCommandOptions): {
566
574
  trigger: options.trigger ?? options.event ?? (table ? `${singular}.created` : undefined),
567
575
  component: options.component,
568
576
  route: options.name ? kebabCase(options.name) : undefined,
569
- withAi: options.withAi,
577
+ withAi: options.withAi || kind === "ai-chat",
570
578
  withCreateForm: options.withCreateForm || kind === "resource",
571
579
  },
572
580
  };
@@ -600,6 +608,77 @@ function addPolicies(plan: MakePlan, workspaceRoot: string, entries: Record<stri
600
608
  plan.diagnostics.push(...next.diagnostics);
601
609
  }
602
610
 
611
+ function addWebAiDependencies(plan: MakePlan, workspaceRoot: string, appName: string): void {
612
+ const desired: Record<string, string> = {
613
+ "@ai-sdk/react": "^3.0.0",
614
+ ai: "^6.0.0",
615
+ };
616
+ const plannedPackage = plan.filesToCreate.find((file) => file.file === "web/package.json");
617
+ if (plannedPackage) {
618
+ try {
619
+ const parsed = JSON.parse(plannedPackage.content) as { dependencies?: Record<string, string> };
620
+ parsed.dependencies = { ...(parsed.dependencies ?? {}), ...desired };
621
+ plannedPackage.content = `${JSON.stringify(parsed, null, 2)}\n`;
622
+ return;
623
+ } catch {
624
+ plan.diagnostics.push(
625
+ diagnostic(
626
+ "warning",
627
+ "FORGE_MAKE_PACKAGE_JSON_UNSUPPORTED_SHAPE",
628
+ "could not update planned web/package.json with AI SDK dependencies",
629
+ "web/package.json",
630
+ ),
631
+ );
632
+ return;
633
+ }
634
+ }
635
+
636
+ const existing = readIfExists(workspaceRoot, "web/package.json");
637
+ if (!existing) {
638
+ plan.filesToCreate.push(
639
+ createFile(
640
+ workspaceRoot,
641
+ "web/package.json",
642
+ "Add web package with AI SDK dependencies",
643
+ renderNextAiPackage(appName),
644
+ ),
645
+ );
646
+ return;
647
+ }
648
+
649
+ try {
650
+ const parsed = JSON.parse(existing) as { dependencies?: Record<string, string> };
651
+ const dependencies = { ...(parsed.dependencies ?? {}) };
652
+ let changed = false;
653
+ for (const [name, version] of Object.entries(desired)) {
654
+ if (dependencies[name] !== version) {
655
+ dependencies[name] = version;
656
+ changed = true;
657
+ }
658
+ }
659
+ if (!changed) {
660
+ return;
661
+ }
662
+ parsed.dependencies = dependencies;
663
+ plan.filesToModify.push({
664
+ file: "web/package.json",
665
+ kind: "replace-section",
666
+ description: "Add AI SDK dependencies to web package",
667
+ beforeHash: hashStable(existing),
668
+ afterPreview: `${JSON.stringify(parsed, null, 2)}\n`,
669
+ });
670
+ } catch {
671
+ plan.diagnostics.push(
672
+ diagnostic(
673
+ "warning",
674
+ "FORGE_MAKE_PACKAGE_JSON_UNSUPPORTED_SHAPE",
675
+ "could not update web/package.json with AI SDK dependencies",
676
+ "web/package.json",
677
+ ),
678
+ );
679
+ }
680
+ }
681
+
603
682
  function addRuntimeFiles(plan: MakePlan, workspaceRoot: string, intent: MakeIntent): void {
604
683
  const table = intent.table ?? intent.name;
605
684
  const singular = singularize(table);
@@ -670,6 +749,50 @@ function addFrontendFiles(plan: MakePlan, workspaceRoot: string, intent: MakeInt
670
749
  const table = intent.table ?? intent.name;
671
750
  const singular = singularize(table);
672
751
  const pascal = pascalCase(singular);
752
+ if (intent.kind === "ai-chat") {
753
+ const name = intent.name || "support";
754
+ const hasViteSource = fileExists(workspaceRoot, "web/src") || fileExists(workspaceRoot, "web/src/main.tsx");
755
+ const bridgeFile = hasViteSource ? "web/src/lib/forge.ts" : "web/lib/forge.ts";
756
+ const componentFile = hasViteSource
757
+ ? `web/src/components/${pascalCase(name)}AiChat.tsx`
758
+ : `web/components/${pascalCase(name)}AiChat.tsx`;
759
+ addWebAiDependencies(plan, workspaceRoot, name);
760
+ if (!fileExists(workspaceRoot, bridgeFile)) {
761
+ plan.filesToCreate.push(
762
+ createFile(
763
+ workspaceRoot,
764
+ bridgeFile,
765
+ "Add Forge client bridge",
766
+ hasViteSource ? renderWebBridge() : renderWebRootBridge(),
767
+ ),
768
+ );
769
+ }
770
+ plan.filesToCreate.push(
771
+ createFile(
772
+ workspaceRoot,
773
+ `src/ai/${camelCase(name)}Agent.ts`,
774
+ `Add AI agent '${name}'`,
775
+ renderAiAgentFile(name),
776
+ ),
777
+ createFile(
778
+ workspaceRoot,
779
+ componentFile,
780
+ `Add AI chat component '${name}'`,
781
+ renderAiChatComponent(name),
782
+ ),
783
+ );
784
+ if (fileExists(workspaceRoot, "web/app") || fileExists(workspaceRoot, "web/app/layout.tsx")) {
785
+ plan.filesToCreate.push(
786
+ createFile(
787
+ workspaceRoot,
788
+ `web/app/${kebabCase(name)}-ai/page.tsx`,
789
+ `Add AI chat page '${name}'`,
790
+ renderAiChatPage(name),
791
+ ),
792
+ );
793
+ }
794
+ return;
795
+ }
673
796
  if (intent.kind === "ui") {
674
797
  plan.filesToCreate.push(
675
798
  createFile(workspaceRoot, "web/package.json", "Add Vite React web package", renderVitePackage(intent.name)),
@@ -351,6 +351,166 @@ export default function ${pascal}Page() {
351
351
  `;
352
352
  }
353
353
 
354
+ export function renderAiAgentFile(name: string): string {
355
+ const camel = camelCase(name);
356
+ const pascal = pascalCase(name);
357
+ return `import { agent, aiTool } from "forge/server";
358
+ import { z } from "zod";
359
+
360
+ export const ${camel}ProjectContext = aiTool({
361
+ description: "Return concise project context for the ${pascal} agent.",
362
+ inputSchema: z.object({
363
+ topic: z.string().optional(),
364
+ }),
365
+ outputSchema: z.object({
366
+ context: z.string(),
367
+ }),
368
+ risk: "read",
369
+ strict: true,
370
+ needsApproval: false,
371
+ handler: async (_ctx, input) => ({
372
+ context: \`ForgeOS project context for \${input.topic ?? "the current request"}.\`,
373
+ }),
374
+ });
375
+
376
+ export const ${camel}Agent = agent({
377
+ provider: "gateway",
378
+ model: "openai/gpt-5.4",
379
+ instructions: "You are a ForgeOS app agent. Use Forge tools before answering when runtime data is needed.",
380
+ tools: { ${camel}ProjectContext },
381
+ stopWhen: { kind: "stepCount", maxSteps: 8 },
382
+ });
383
+ `;
384
+ }
385
+
386
+ export function renderAiChatComponent(name: string): string {
387
+ const pascal = pascalCase(name);
388
+ return `"use client";
389
+
390
+ import { DefaultChatTransport } from "ai";
391
+ import { useChat } from "@ai-sdk/react";
392
+ import { FormEvent, useMemo, useState } from "react";
393
+ import { forgeUrl } from "../lib/forge";
394
+
395
+ export function ${pascal}AiChat() {
396
+ const [input, setInput] = useState("");
397
+ const transport = useMemo(
398
+ () =>
399
+ new DefaultChatTransport({
400
+ api: \`\${forgeUrl}/ai/agents/chat\`,
401
+ headers: {
402
+ "x-forge-user-id": "dev-user",
403
+ "x-forge-tenant-id": "dev-tenant",
404
+ "x-forge-role": "owner",
405
+ },
406
+ body: {
407
+ agent: "${camelCase(name)}Agent",
408
+ provider: "gateway",
409
+ model: "openai/gpt-5.4",
410
+ instructions: "Answer as a ForgeOS app agent. Use available Forge tools when useful.",
411
+ maxSteps: 8,
412
+ },
413
+ }),
414
+ [],
415
+ );
416
+ const { messages, sendMessage, status, error, addToolApprovalResponse } = useChat({
417
+ transport,
418
+ });
419
+ const busy = status === "submitted" || status === "streaming";
420
+
421
+ async function submit(event: FormEvent<HTMLFormElement>) {
422
+ event.preventDefault();
423
+ const prompt = input.trim();
424
+ if (!prompt || busy) return;
425
+ setInput("");
426
+ sendMessage({ text: prompt });
427
+ }
428
+
429
+ return (
430
+ <section>
431
+ <div>
432
+ {messages.map((message) => (
433
+ <article key={message.id}>
434
+ <strong>{message.role}</strong>
435
+ {message.parts.map((part, index) => {
436
+ if (part.type === "text") {
437
+ return <p key={index}>{part.text}</p>;
438
+ }
439
+ if (part.type.startsWith("tool-")) {
440
+ const toolPart = part as typeof part & {
441
+ input?: unknown;
442
+ output?: unknown;
443
+ approval?: { id: string; state: string };
444
+ };
445
+ return (
446
+ <div key={index}>
447
+ <code>{part.type.replace(/^tool-/, "")}</code>
448
+ {toolPart.approval?.state === "approval-requested" ? (
449
+ <span>
450
+ <button
451
+ type="button"
452
+ onClick={() =>
453
+ addToolApprovalResponse({
454
+ id: toolPart.approval!.id,
455
+ approved: true,
456
+ })
457
+ }
458
+ >
459
+ Approve
460
+ </button>
461
+ <button
462
+ type="button"
463
+ onClick={() =>
464
+ addToolApprovalResponse({
465
+ id: toolPart.approval!.id,
466
+ approved: false,
467
+ })
468
+ }
469
+ >
470
+ Deny
471
+ </button>
472
+ </span>
473
+ ) : null}
474
+ </div>
475
+ );
476
+ }
477
+ return null;
478
+ })}
479
+ </article>
480
+ ))}
481
+ </div>
482
+ <form onSubmit={submit}>
483
+ <input
484
+ value={input}
485
+ onChange={(event) => setInput(event.currentTarget.value)}
486
+ placeholder="Ask the Forge agent"
487
+ />
488
+ <button type="submit" disabled={busy}>{busy ? "Running" : "Send"}</button>
489
+ </form>
490
+ {error ? <p>{error.message}</p> : null}
491
+ </section>
492
+ );
493
+ }
494
+ `;
495
+ }
496
+
497
+ export function renderAiChatPage(name: string): string {
498
+ const pascal = pascalCase(name);
499
+ return `"use client";
500
+
501
+ import { ${pascal}AiChat } from "../../components/${pascal}AiChat";
502
+
503
+ export default function ${pascal}AiPage() {
504
+ return (
505
+ <main>
506
+ <h1>${titleCase(name)} AI</h1>
507
+ <${pascal}AiChat />
508
+ </main>
509
+ );
510
+ }
511
+ `;
512
+ }
513
+
354
514
  export function renderWebBridge(): string {
355
515
  return `export const forgeUrl =
356
516
  import.meta.env.VITE_FORGE_URL ?? "http://127.0.0.1:3765";
@@ -476,7 +636,9 @@ export function renderVitePackage(appName: string): string {
476
636
  "typecheck": "tsc --noEmit"
477
637
  },
478
638
  "dependencies": {
639
+ "@ai-sdk/react": "^3.0.0",
479
640
  "@vitejs/plugin-react": "^4.3.4",
641
+ "ai": "^6.0.0",
480
642
  "vite": "^6.0.5",
481
643
  "react": "^19.0.0",
482
644
  "react-dom": "^19.0.0"
@@ -490,6 +652,32 @@ export function renderVitePackage(appName: string): string {
490
652
  `;
491
653
  }
492
654
 
655
+ export function renderNextAiPackage(appName: string): string {
656
+ return `{
657
+ "name": "${kebabCase(appName)}-web",
658
+ "private": true,
659
+ "type": "module",
660
+ "scripts": {
661
+ "dev": "next dev --hostname 127.0.0.1",
662
+ "build": "next build",
663
+ "typecheck": "tsc --noEmit"
664
+ },
665
+ "dependencies": {
666
+ "@ai-sdk/react": "^3.0.0",
667
+ "ai": "^6.0.0",
668
+ "next": "^15.5.9",
669
+ "react": "^19.0.0",
670
+ "react-dom": "^19.0.0"
671
+ },
672
+ "devDependencies": {
673
+ "@types/react": "^19.0.0",
674
+ "@types/react-dom": "^19.0.0",
675
+ "typescript": "^5.7.3"
676
+ }
677
+ }
678
+ `;
679
+ }
680
+
493
681
  export function renderViteTsconfig(): string {
494
682
  return `{
495
683
  "compilerOptions": {
@@ -14,6 +14,7 @@ export type MakePrimitive =
14
14
  | "component"
15
15
  | "page"
16
16
  | "ui"
17
+ | "ai-chat"
17
18
  | "resource"
18
19
  | "apply"
19
20
  | "rollback";
@@ -15,6 +15,8 @@ import type {
15
15
  ForgeGenerateStructuredInput,
16
16
  ForgeGenerateTextInput,
17
17
  ForgeGenerateTextResult,
18
+ ForgeRunAgentInput,
19
+ ForgeRunAgentResult,
18
20
  ForgeStreamTextInput,
19
21
  ForgeStreamTextResult,
20
22
  ForgeAiUsage,
@@ -64,6 +66,10 @@ export interface CreateAiContextOptions {
64
66
  runtimeKind: RuntimeContext;
65
67
  envelope?: AiTelemetryEnvelope;
66
68
  mockAi?: boolean;
69
+ toolContext?: {
70
+ env?: Record<string, string | undefined>;
71
+ auth?: unknown;
72
+ };
67
73
  }
68
74
 
69
75
  export function aiForbiddenInContext(runtimeKind: RuntimeContext): boolean {
@@ -94,6 +100,12 @@ export function createAiContext(options: CreateAiContextOptions): AiContext {
94
100
  `ctx.ai is forbidden in '${runtimeKind}' context`,
95
101
  );
96
102
  },
103
+ async runAgent() {
104
+ forgeError(
105
+ FORGE_AI_FORBIDDEN_CONTEXT,
106
+ `ctx.ai is forbidden in '${runtimeKind}' context`,
107
+ );
108
+ },
97
109
  };
98
110
  }
99
111
 
@@ -284,7 +296,7 @@ export function createAiContext(options: CreateAiContextOptions): AiContext {
284
296
 
285
297
  return {
286
298
  textStream: result.textStream,
287
- text: result.text.then(async (text) => {
299
+ text: Promise.resolve(result.text).then(async (text) => {
288
300
  const usage = mapUsage(await result.usage);
289
301
  await recordAiTelemetry(telemetry, "forge.ai.stream.completed", {
290
302
  ...baseProps(),
@@ -300,7 +312,7 @@ export function createAiContext(options: CreateAiContextOptions): AiContext {
300
312
  provider: input.provider,
301
313
  model: input.model,
302
314
  purpose: input.purpose,
303
- usage: result.usage.then(mapUsage),
315
+ usage: Promise.resolve(result.usage).then(mapUsage),
304
316
  latencyMs,
305
317
  };
306
318
  },
@@ -347,10 +359,10 @@ export function createAiContext(options: CreateAiContextOptions): AiContext {
347
359
  );
348
360
  const { generateText, Output } = await import("ai");
349
361
  const result = await generateText({
350
- model: languageModel,
362
+ model: languageModel as never,
351
363
  prompt: input.prompt,
352
364
  system: input.system,
353
- experimental_output: Output.object({ schema: input.schema as never }),
365
+ output: Output.object({ schema: input.schema as never }),
354
366
  });
355
367
 
356
368
  const usage = mapUsage(result.usage);
@@ -365,7 +377,7 @@ export function createAiContext(options: CreateAiContextOptions): AiContext {
365
377
  method: "generateStructured",
366
378
  });
367
379
 
368
- return result.experimental_output as T;
380
+ return result.output as T;
369
381
  } catch (error) {
370
382
  const message = error instanceof Error ? error.message : String(error);
371
383
  await recordAiTelemetry(telemetry, "forge.ai.generation.failed", {
@@ -379,6 +391,198 @@ export function createAiContext(options: CreateAiContextOptions): AiContext {
379
391
  throw error;
380
392
  }
381
393
  },
394
+
395
+ async runAgent(input: ForgeRunAgentInput): Promise<ForgeRunAgentResult> {
396
+ const provider = input.provider ?? "gateway";
397
+ const startedAt = Date.now();
398
+ await recordAiTelemetry(telemetry, "forge.ai.agent.started", {
399
+ ...baseProps(),
400
+ provider,
401
+ model: input.model,
402
+ purpose: input.purpose,
403
+ toolCount: Object.keys(input.tools ?? {}).length,
404
+ });
405
+
406
+ try {
407
+ if (useMock) {
408
+ const mock = dequeueMockAiResponse();
409
+ const usage = createMockAiUsage(mock.usage);
410
+ const latencyMs = Date.now() - startedAt;
411
+ const estimatedCostUsd = estimateCostUsd(provider, input.model, usage);
412
+ const result = {
413
+ text: mock.text,
414
+ provider,
415
+ model: input.model,
416
+ purpose: input.purpose,
417
+ usage,
418
+ latencyMs,
419
+ toolCalls: [],
420
+ toolResults: [],
421
+ steps: 1,
422
+ estimatedCostUsd,
423
+ };
424
+ await recordAiTelemetry(telemetry, "forge.ai.agent.completed", {
425
+ ...baseProps(),
426
+ provider,
427
+ model: input.model,
428
+ purpose: input.purpose,
429
+ latencyMs,
430
+ usage,
431
+ estimatedCostUsd,
432
+ status: "completed",
433
+ mode: "mock",
434
+ });
435
+ return result;
436
+ }
437
+
438
+ const languageModel = await resolveLanguageModel(provider, input.model, secrets);
439
+ const { ToolLoopAgent, hasToolCall, stepCountIs, tool } = await import("ai");
440
+ const toolRuntimeContext = {
441
+ secrets,
442
+ env: options.toolContext?.env ?? {},
443
+ telemetry,
444
+ auth: options.toolContext?.auth,
445
+ };
446
+
447
+ const tools = Object.fromEntries(
448
+ Object.entries(input.tools ?? {}).map(([name, definition]) => {
449
+ const needsApproval = definition.needsApproval;
450
+ const aiSdkToolConfig = {
451
+ description: definition.description,
452
+ inputSchema: definition.inputSchema as never,
453
+ ...(definition.outputSchema
454
+ ? { outputSchema: definition.outputSchema as never }
455
+ : {}),
456
+ ...(definition.strict !== undefined ? { strict: definition.strict } : {}),
457
+ ...(needsApproval !== undefined
458
+ ? {
459
+ needsApproval:
460
+ typeof needsApproval === "function"
461
+ ? async ({ args }: { args: unknown }) =>
462
+ Boolean(await needsApproval(args as never))
463
+ : needsApproval,
464
+ }
465
+ : {}),
466
+ execute: async (args: unknown) => {
467
+ await recordAiTelemetry(telemetry, "forge.ai.tool.started", {
468
+ ...baseProps(),
469
+ provider,
470
+ model: input.model,
471
+ purpose: input.purpose,
472
+ tool: name,
473
+ risk: definition.risk ?? "external",
474
+ });
475
+ const toolStartedAt = Date.now();
476
+ try {
477
+ const output = await definition.handler(
478
+ toolRuntimeContext,
479
+ args as never,
480
+ );
481
+ await recordAiTelemetry(telemetry, "forge.ai.tool.completed", {
482
+ ...baseProps(),
483
+ provider,
484
+ model: input.model,
485
+ purpose: input.purpose,
486
+ tool: name,
487
+ latencyMs: Date.now() - toolStartedAt,
488
+ status: "completed",
489
+ });
490
+ return output;
491
+ } catch (error) {
492
+ await recordAiTelemetry(telemetry, "forge.ai.tool.failed", {
493
+ ...baseProps(),
494
+ provider,
495
+ model: input.model,
496
+ purpose: input.purpose,
497
+ tool: name,
498
+ latencyMs: Date.now() - toolStartedAt,
499
+ status: "failed",
500
+ error: error instanceof Error ? error.message : String(error),
501
+ });
502
+ throw error;
503
+ }
504
+ },
505
+ };
506
+ return [name, tool(aiSdkToolConfig as never)];
507
+ }),
508
+ );
509
+
510
+ const stopWhen =
511
+ input.stopWhen?.kind === "toolCall"
512
+ ? hasToolCall(input.stopWhen.toolName)
513
+ : stepCountIs(input.stopWhen?.maxSteps ?? input.maxSteps ?? 20);
514
+
515
+ const agent = new ToolLoopAgent({
516
+ model: languageModel as never,
517
+ instructions: input.instructions,
518
+ tools,
519
+ stopWhen,
520
+ temperature: input.temperature,
521
+ maxOutputTokens: input.maxTokens,
522
+ });
523
+
524
+ const result = await agent.generate({ prompt: input.prompt });
525
+ const usage = mapUsage(result.usage);
526
+ const latencyMs = Date.now() - startedAt;
527
+ const estimatedCostUsd = estimateCostUsd(provider, input.model, usage);
528
+ const raw = result as unknown as {
529
+ steps?: unknown[];
530
+ toolCalls?: Array<{ toolName?: string; input?: unknown; args?: unknown }>;
531
+ toolResults?: Array<{ toolName?: string; output?: unknown; result?: unknown }>;
532
+ };
533
+
534
+ await recordAiTelemetry(telemetry, "forge.ai.agent.completed", {
535
+ ...baseProps(),
536
+ provider,
537
+ model: input.model,
538
+ purpose: input.purpose,
539
+ latencyMs,
540
+ usage,
541
+ estimatedCostUsd,
542
+ status: "completed",
543
+ steps: raw.steps?.length ?? 1,
544
+ });
545
+ await recordAiTelemetry(telemetry, "forge.ai.usage", {
546
+ ...baseProps(),
547
+ provider,
548
+ model: input.model,
549
+ purpose: input.purpose,
550
+ usage,
551
+ estimatedCostUsd,
552
+ method: "runAgent",
553
+ });
554
+
555
+ return {
556
+ text: result.text,
557
+ provider,
558
+ model: input.model,
559
+ purpose: input.purpose,
560
+ usage,
561
+ latencyMs,
562
+ toolCalls: (raw.toolCalls ?? []).map((call) => ({
563
+ toolName: call.toolName ?? "unknown",
564
+ input: call.input ?? call.args,
565
+ })),
566
+ toolResults: (raw.toolResults ?? []).map((toolResult) => ({
567
+ toolName: toolResult.toolName ?? "unknown",
568
+ output: toolResult.output ?? toolResult.result,
569
+ })),
570
+ steps: raw.steps?.length ?? 1,
571
+ estimatedCostUsd,
572
+ };
573
+ } catch (error) {
574
+ const message = error instanceof Error ? error.message : String(error);
575
+ await recordAiTelemetry(telemetry, "forge.ai.agent.failed", {
576
+ ...baseProps(),
577
+ provider,
578
+ model: input.model,
579
+ purpose: input.purpose,
580
+ status: "failed",
581
+ error: message,
582
+ });
583
+ throw error;
584
+ }
585
+ },
382
586
  };
383
587
  }
384
588
 
@@ -390,5 +594,6 @@ export function createNoopAiContext(): AiContext {
390
594
  generateText: noop,
391
595
  streamText: noop,
392
596
  generateStructured: noop,
597
+ runAgent: noop,
393
598
  };
394
599
  }