forgeos 0.1.0-alpha.2 → 0.1.0-alpha.3
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/AGENTS.md +38 -3
- package/README.md +6 -5
- package/package.json +5 -4
- package/src/forge/_generated/actionSubscriptions.json +2 -2
- package/src/forge/_generated/actionSubscriptions.ts +3 -3
- package/src/forge/_generated/agentAdapterManifest.json +2 -2
- package/src/forge/_generated/agentAdapterManifest.ts +3 -3
- package/src/forge/_generated/agentContract.json +2 -2
- package/src/forge/_generated/agentContract.ts +183 -50
- package/src/forge/_generated/agentQuickstart.md +3 -1
- package/src/forge/_generated/agentTools.json +2 -0
- package/src/forge/_generated/agentTools.md +16 -0
- package/src/forge/_generated/agentTools.ts +12 -0
- package/src/forge/_generated/aiContext.ts +67 -1
- package/src/forge/_generated/aiModels.json +2 -2
- package/src/forge/_generated/aiModels.ts +17 -1
- package/src/forge/_generated/aiProviders.json +1 -1
- package/src/forge/_generated/aiProviders.ts +1 -1
- package/src/forge/_generated/aiRegistry.json +2 -2
- package/src/forge/_generated/aiRegistry.ts +7 -5
- package/src/forge/_generated/api.json +2 -2
- package/src/forge/_generated/api.ts +1 -1
- package/src/forge/_generated/appGraph.json +2 -2
- package/src/forge/_generated/appGraph.ts +288 -180
- package/src/forge/_generated/appMap.md +21 -1
- package/src/forge/_generated/artifactManifest.json +2 -2
- package/src/forge/_generated/artifactManifest.ts +2 -2
- package/src/forge/_generated/authClaims.json +1 -1
- package/src/forge/_generated/authClaims.ts +1 -1
- package/src/forge/_generated/authConfig.json +1 -1
- package/src/forge/_generated/authConfig.ts +1 -1
- package/src/forge/_generated/authContext.ts +1 -1
- package/src/forge/_generated/authRegistry.json +1 -1
- package/src/forge/_generated/authRegistry.ts +1 -1
- package/src/forge/_generated/buildInfo.json +2 -2
- package/src/forge/_generated/buildInfo.ts +4 -4
- package/src/forge/_generated/capabilityMap.json +2 -2
- package/src/forge/_generated/capabilityMap.md +1 -1
- package/src/forge/_generated/capabilityMap.ts +2 -2
- package/src/forge/_generated/client.ts +1 -1
- package/src/forge/_generated/clientApi.ts +1 -1
- package/src/forge/_generated/clientManifest.json +2 -2
- package/src/forge/_generated/clientManifest.ts +3 -3
- package/src/forge/_generated/clientTypes.ts +1 -1
- package/src/forge/_generated/configRegistry.json +1 -1
- package/src/forge/_generated/configRegistry.ts +1 -1
- package/src/forge/_generated/dataGraph.json +2 -2
- package/src/forge/_generated/dataGraph.ts +3 -3
- package/src/forge/_generated/db.json +1 -1
- package/src/forge/_generated/db.ts +1 -1
- package/src/forge/_generated/dbSecurityManifest.json +1 -1
- package/src/forge/_generated/dbSecurityManifest.ts +1 -1
- package/src/forge/_generated/dbSessionContext.json +1 -1
- package/src/forge/_generated/dbSessionContext.ts +1 -1
- package/src/forge/_generated/deployManifest.json +2 -2
- package/src/forge/_generated/deployManifest.ts +7 -7
- package/src/forge/_generated/devManifest.json +2 -2
- package/src/forge/_generated/devManifest.ts +18 -3
- package/src/forge/_generated/envSchema.json +1 -1
- package/src/forge/_generated/envSchema.ts +1 -1
- package/src/forge/_generated/frontendGraph.json +1 -1
- package/src/forge/_generated/frontendGraph.ts +1 -1
- package/src/forge/_generated/importGuards.json +1 -1
- package/src/forge/_generated/importGuards.ts +1 -1
- package/src/forge/_generated/index.ts +2 -1
- package/src/forge/_generated/liveProductionManifest.json +1 -1
- package/src/forge/_generated/liveProductionManifest.ts +1 -1
- package/src/forge/_generated/liveProtocol.json +1 -1
- package/src/forge/_generated/liveProtocol.ts +1 -1
- package/src/forge/_generated/liveQueryRegistry.json +2 -2
- package/src/forge/_generated/liveQueryRegistry.ts +3 -3
- package/src/forge/_generated/liveTransportConfig.json +1 -1
- package/src/forge/_generated/liveTransportConfig.ts +1 -1
- package/src/forge/_generated/makeRegistry.json +2 -2
- package/src/forge/_generated/makeRegistry.ts +16 -2
- package/src/forge/_generated/makeTemplates.json +2 -2
- package/src/forge/_generated/makeTemplates.ts +6 -1
- package/src/forge/_generated/mockMap.json +1 -1
- package/src/forge/_generated/mockMap.ts +1 -1
- package/src/forge/_generated/operationPlaybooks.md +34 -14
- package/src/forge/_generated/packageGraph.json +2 -2
- package/src/forge/_generated/packageGraph.ts +8808 -4723
- package/src/forge/_generated/packageUpgradeRegistry.json +2 -2
- package/src/forge/_generated/packageUpgradeRegistry.ts +2 -2
- package/src/forge/_generated/permissionMatrix.json +2 -2
- package/src/forge/_generated/permissionMatrix.ts +3 -3
- package/src/forge/_generated/policyRegistry.json +2 -2
- package/src/forge/_generated/policyRegistry.ts +3 -3
- package/src/forge/_generated/queryRegistry.json +2 -2
- package/src/forge/_generated/queryRegistry.ts +3 -3
- package/src/forge/_generated/react.d.ts +1 -1
- package/src/forge/_generated/react.ts +1 -1
- package/src/forge/_generated/reactManifest.json +2 -2
- package/src/forge/_generated/reactManifest.ts +3 -3
- package/src/forge/_generated/releaseManifest.json +2 -2
- package/src/forge/_generated/releaseManifest.ts +3 -3
- package/src/forge/_generated/rlsPolicies.json +1 -1
- package/src/forge/_generated/rlsPolicies.sql +1 -1
- package/src/forge/_generated/rlsPolicies.ts +1 -1
- package/src/forge/_generated/runtimeGraph.json +2 -2
- package/src/forge/_generated/runtimeGraph.ts +3 -3
- package/src/forge/_generated/runtimeMatrix.json +2 -2
- package/src/forge/_generated/runtimeMatrix.ts +8684 -1939
- package/src/forge/_generated/runtimeRegistry.ts +1 -1
- package/src/forge/_generated/runtimeRules.md +13 -1
- package/src/forge/_generated/secretRegistry.json +1 -1
- package/src/forge/_generated/secretRegistry.ts +1 -1
- package/src/forge/_generated/secretsContext.ts +1 -1
- package/src/forge/_generated/serverApi.ts +1 -1
- package/src/forge/_generated/sourceMapManifest.json +2 -2
- package/src/forge/_generated/sourceMapManifest.ts +2 -2
- package/src/forge/_generated/sqlPlan.json +1 -1
- package/src/forge/_generated/sqlPlan.ts +1 -1
- package/src/forge/_generated/subscriptionManifest.json +2 -2
- package/src/forge/_generated/subscriptionManifest.ts +3 -3
- package/src/forge/_generated/symbolicationManifest.json +2 -2
- package/src/forge/_generated/symbolicationManifest.ts +2 -2
- package/src/forge/_generated/telemetryRegistry.json +2 -2
- package/src/forge/_generated/telemetryRegistry.ts +3 -3
- package/src/forge/_generated/telemetrySinks.json +2 -2
- package/src/forge/_generated/telemetrySinks.ts +2 -2
- package/src/forge/_generated/tenantScope.json +2 -2
- package/src/forge/_generated/tenantScope.ts +3 -3
- package/src/forge/_generated/testGraph.json +2 -2
- package/src/forge/_generated/testGraph.ts +17 -7
- package/src/forge/_generated/testPlanRegistry.json +2 -2
- package/src/forge/_generated/testPlanRegistry.ts +2 -2
- package/src/forge/_generated/uiRoutes.json +1 -1
- package/src/forge/_generated/uiRoutes.ts +1 -1
- package/src/forge/_generated/uiScenarios.json +1 -1
- package/src/forge/_generated/uiScenarios.ts +1 -1
- package/src/forge/_generated/uiTestManifest.json +2 -2
- package/src/forge/_generated/uiTestManifest.ts +2 -2
- package/src/forge/_generated/workflowRegistry.json +2 -2
- package/src/forge/_generated/workflowRegistry.ts +3 -3
- package/src/forge/_generated/workflowSubscriptions.json +2 -2
- package/src/forge/_generated/workflowSubscriptions.ts +3 -3
- package/src/forge/cli/ai.ts +186 -1
- package/src/forge/cli/commands.ts +5 -0
- package/src/forge/cli/parse.ts +30 -3
- package/src/forge/compiler/agent-contract/build.ts +281 -8
- package/src/forge/compiler/agent-contract/types.ts +41 -0
- package/src/forge/compiler/ai-registry/build.ts +62 -1
- package/src/forge/compiler/ai-registry/constants.ts +1 -1
- package/src/forge/compiler/ai-registry/parse.ts +98 -4
- package/src/forge/compiler/app-graph/forge-apis.ts +1 -0
- package/src/forge/compiler/dev-manifest/build.ts +3 -0
- package/src/forge/compiler/make-registry/build.ts +13 -0
- package/src/forge/compiler/orchestrator/plan.ts +11 -0
- package/src/forge/compiler/orchestrator/serialize.ts +68 -0
- package/src/forge/compiler/types/ai-registry.ts +25 -1
- package/src/forge/compiler/types/app-graph.ts +1 -0
- package/src/forge/compiler/types/cli.ts +1 -0
- package/src/forge/compiler/types/dev-manifest.ts +3 -0
- package/src/forge/dev/server.ts +508 -1
- package/src/forge/make/index.ts +126 -3
- package/src/forge/make/templates.ts +188 -0
- package/src/forge/make/types.ts +1 -0
- package/src/forge/runtime/ai/context.ts +210 -5
- package/src/forge/runtime/ai/types.ts +70 -0
- package/src/forge/runtime/context/create-context.ts +30 -6
- package/src/forge/server.ts +82 -0
- package/src/forge/version.ts +1 -1
package/src/forge/make/index.ts
CHANGED
|
@@ -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": {
|
package/src/forge/make/types.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
}
|