diff --git a/ts/docs/architecture/agent-patterns.md b/ts/docs/architecture/agent-patterns.md index e813988442..8cce943d15 100644 --- a/ts/docs/architecture/agent-patterns.md +++ b/ts/docs/architecture/agent-patterns.md @@ -178,7 +178,18 @@ plugin/ (or extension/) ← connects to the bridge and calls host APIs ``` -**AppAgent lifecycle:** implements `closeAgentContext()` to stop the server. +**Port allocation:** the bridge binds on an OS-assigned ephemeral port +(`port: 0`) by default. The actual port is registered with the +dispatcher via `context.registerPort("default", port)` and is +discoverable by external clients through the agent-server's discovery +channel (`discoverPort("", "default")`). Set the +`_BRIDGE_PORT` environment variable to pin the bridge to a fixed +port when debugging. The server uses a refcounted shared-instance +pattern so multiple sessions reuse one listener. + +**AppAgent lifecycle:** `updateAgentContext` starts/stops the server +per session; `closeAgentContext` is the backstop that releases the +registration and closes the server if disable wasn't called. **Dependencies added:** `ws` @@ -240,21 +251,32 @@ system service that exposes no REST API. ### 8. `view-ui` — Web View Renderer A minimal action handler that opens a local HTTP server serving a `site/` -directory and signals the dispatcher to open the view via `openLocalView`. -The actual UX lives in the `site/` directory; the handler communicates with -it via display APIs and IPC types. +directory and signals the shell to load it. The actual UX lives in the +`site/` directory; the handler communicates with it via display APIs +and IPC types. **File layout** ``` src/ - ActionHandler.ts ← opens/closes view, handles actions + ActionHandler.ts ← opens/closes view server, handles actions ipcTypes.ts ← shared message types for handler ↔ view IPC site/ index.html ← web view entry point ... ``` +**Port allocation:** the view server binds on an OS-assigned ephemeral +port (`port: 0`) by default during `updateAgentContext(true)`. The +actual port is registered with the dispatcher via +`context.registerPort("view", port)` for out-of-process discovery +(`discoverPort("", "view")`) and also passed to +`context.setLocalHostPort(port)` so the embedding shell knows which URL +to load. Set the `_VIEW_PORT` environment variable to pin the +view to a fixed port when debugging. The view is surfaced in the shell +by returning an `ActivityContext` with `openLocalView: true` from +`executeAction`. + **Manifest flags:** `"localView": true` **When to choose:** agents that need a rich interactive UI beyond simple text diff --git a/ts/packages/agents/onboarding/package.json b/ts/packages/agents/onboarding/package.json index bb35f29be5..37a6ce105c 100644 --- a/ts/packages/agents/onboarding/package.json +++ b/ts/packages/agents/onboarding/package.json @@ -54,12 +54,15 @@ "devDependencies": { "@typeagent/action-schema-compiler": "workspace:*", "@types/debug": "^4.1.12", + "@types/node": "^22.0.0", + "@types/ws": "^8.5.10", "action-grammar-compiler": "workspace:*", "concurrently": "^9.1.2", "copyfiles": "^2.4.1", "prettier": "^3.5.3", "rimraf": "^6.0.1", - "typescript": "~5.4.5" + "typescript": "~5.4.5", + "ws": "^8.18.0" }, "engines": { "node": ">=22" diff --git a/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts b/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts index 76fcc3b12c..fc9685dc92 100644 --- a/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts +++ b/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts @@ -21,6 +21,7 @@ import { } from "../lib/workspace.js"; import type { ApiSurface } from "../discovery/discoveryHandler.js"; import { buildCliHandler } from "./cliHandlerTemplate.js"; +import { loadTemplate } from "./templateLoader.js"; import fs from "fs/promises"; import path from "path"; import { fileURLToPath } from "url"; @@ -690,39 +691,10 @@ async function buildHandler( } function buildSchemaGrammarHandler(name: string, pascalName: string): string { - return `// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { - ActionContext, - AppAgent, - TypeAgentAction, - ActionResult, -} from "@typeagent/agent-sdk"; -import { createActionResultFromTextDisplay } from "@typeagent/agent-sdk/helpers/action"; -import { ${pascalName}Actions } from "./${name}Schema.js"; - -export function instantiate(): AppAgent { - return { - initializeAgentContext, - executeAction, - }; -} - -async function initializeAgentContext(): Promise { - return {}; -} - -async function executeAction( - action: TypeAgentAction<${pascalName}Actions>, - context: ActionContext, -): Promise { - // TODO: implement action handlers - return createActionResultFromTextDisplay( - \`Executing \${action.actionName} — not yet implemented.\`, - ); -} -`; + return loadTemplate("schemaGrammarHandler.ts", { + agentName: name, + AgentName: pascalName, + }); } // Map of TypeAgent workspace packages to their location relative to the @@ -874,7 +846,7 @@ const PLUGIN_TEMPLATES: Record< "WebSocket bridge (bidirectional RPC, used by Excel, VS Code agents)", defaultSubdir: "src", nextSteps: - "Start the bridge with `new WebSocketBridge(port).start()` and connect your plugin.", + 'Bind on an OS-assigned port via `await Bridge.start()` (replace `` with your agent name), then publish the bound `.port` from your handler with `context.registerPort("default", bridge.port)` so external clients can discover it.', files: (name) => ({ [`${name}Bridge.ts`]: buildWebSocketBridgeTemplate(name), }), @@ -893,161 +865,40 @@ const PLUGIN_TEMPLATES: Record< }; function buildRestClientTemplate(name: string): string { - return `// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -// REST client bridge for ${name}. -// Calls the target API and returns results to the TypeAgent handler. - -export class ${toPascalCase(name)}Bridge { - constructor(private readonly baseUrl: string, private readonly apiKey?: string) {} - - async executeCommand(actionName: string, parameters: Record): Promise { - // TODO: map actionName to HTTP endpoint and method - throw new Error(\`Not implemented: \${actionName}\`); - } - - private get headers(): Record { - const h: Record = { "Content-Type": "application/json" }; - if (this.apiKey) h["Authorization"] = \`Bearer \${this.apiKey}\`; - return h; - } -} -`; + const pascalName = toPascalCase(name); + return loadTemplate("restClientTemplate.ts", { + agentName: name, + AgentName: pascalName, + }); } function buildWebSocketBridgeTemplate(name: string): string { - return `// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -// WebSocket bridge for ${name}. -// Manages a WebSocket connection to the host application plugin. -// Pattern matches the Excel/VS Code agent bridge implementations. - -import { WebSocketServer, WebSocket } from "ws"; - -type BridgeCommand = { - id: string; - actionName: string; - parameters: Record; -}; - -type BridgeResponse = { - id: string; - success: boolean; - result?: unknown; - error?: string; -}; - -export class ${toPascalCase(name)}Bridge { - private wss: WebSocketServer | undefined; - private client: WebSocket | undefined; - private pending = new Map void>(); - - constructor(private readonly port: number) {} - - start(): void { - this.wss = new WebSocketServer({ port: this.port }); - this.wss.on("connection", (ws) => { - this.client = ws; - ws.on("message", (data) => { - const response = JSON.parse(data.toString()) as BridgeResponse; - this.pending.get(response.id)?.(response); - this.pending.delete(response.id); - }); - }); - } - - async sendCommand(actionName: string, parameters: Record): Promise { - if (!this.client) throw new Error("No client connected"); - const id = \`cmd-\${Date.now()}-\${Math.random().toString(36).slice(2)}\`; - return new Promise((resolve, reject) => { - this.pending.set(id, (res) => { - if (res.success) resolve(res.result); - else reject(new Error(res.error)); - }); - this.client!.send(JSON.stringify({ id, actionName, parameters } satisfies BridgeCommand)); - }); - } -} -`; + const pascalName = toPascalCase(name); + return loadTemplate("websocketBridgeTemplate.ts", { + agentName: name, + AgentName: pascalName, + }); } function buildOfficeAddinHtml(name: string): string { - return ` - - - - ${toPascalCase(name)} TypeAgent Add-in - - - - -

${toPascalCase(name)} TypeAgent

-
Connecting...
- - -`; + const pascalName = toPascalCase(name); + return loadTemplate("officeAddinHtml.template", { + AgentName: pascalName, + }); } function buildOfficeAddinTs(name: string): string { - return `// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -// Office.js task pane add-in for ${name} TypeAgent integration. -// Connects to the TypeAgent bridge via WebSocket and forwards commands -// to the Office.js API. - -const BRIDGE_PORT = 5678; - -Office.onReady(async () => { - document.getElementById("status")!.textContent = "Connecting to TypeAgent..."; - const ws = new WebSocket(\`ws://localhost:\${BRIDGE_PORT}\`); - - ws.onopen = () => { - document.getElementById("status")!.textContent = "Connected"; - ws.send(JSON.stringify({ type: "hello", addinName: "${name}" })); - }; - - ws.onmessage = async (event) => { - const command = JSON.parse(event.data); - try { - const result = await executeCommand(command.actionName, command.parameters); - ws.send(JSON.stringify({ id: command.id, success: true, result })); - } catch (err: any) { - ws.send(JSON.stringify({ id: command.id, success: false, error: err?.message ?? String(err) })); - } - }; -}); - -async function executeCommand(actionName: string, parameters: Record): Promise { - // TODO: map actionName to Office.js API calls - throw new Error(\`Not implemented: \${actionName}\`); -} -`; + return loadTemplate("officeAddinTs.template", { + agentName: name, + BRIDGE_PORT: "5678", + }); } function buildOfficeManifestXml(name: string): string { const pascal = toPascalCase(name); - return ` - - - 1.0.0.0 - Microsoft - en-US - - - - - - - - - ReadWriteDocument - -`; + return loadTemplate("officeManifestXml.template", { + AgentName: pascal, + }); } async function writeFile(filePath: string, content: string): Promise { @@ -1081,554 +932,63 @@ async function handleListPatterns(): Promise { // ─── Pattern-specific handler builders ─────────────────────────────────────── function buildExternalApiHandler(name: string, pascalName: string): string { - return `// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -// Pattern: external-api — REST/OAuth cloud API bridge. -// Implement ${pascalName}Client with your API's authentication and endpoints. - -import { - ActionContext, - AppAgent, - SessionContext, - TypeAgentAction, - ActionResult, -} from "@typeagent/agent-sdk"; -import { createActionResultFromTextDisplay } from "@typeagent/agent-sdk/helpers/action"; -import { ${pascalName}Actions } from "./${name}Schema.js"; - -// ---- API client -------------------------------------------------------- - -class ${pascalName}Client { - private token: string | undefined; - - /** Authenticate and store the access token. */ - async authenticate(): Promise { - // TODO: implement OAuth flow or API key loading. - // Store token in: ~/.typeagent/profiles//${name}/token.json - throw new Error("authenticate() not yet implemented"); - } - - async callApi(endpoint: string, params: Record): Promise { - if (!this.token) await this.authenticate(); - // TODO: implement HTTP call using this.token - throw new Error(\`callApi(\${endpoint}) not yet implemented\`); - } -} - -// ---- Agent lifecycle --------------------------------------------------- - -type Context = { client: ${pascalName}Client }; - -export function instantiate(): AppAgent { - return { - initializeAgentContext, - updateAgentContext, - executeAction, - }; -} - -async function initializeAgentContext(): Promise { - return { client: new ${pascalName}Client() }; -} - -async function updateAgentContext( - _enable: boolean, - _context: SessionContext, - _schemaName: string, -): Promise { - // Optionally authenticate eagerly when the agent is enabled. -} - -async function executeAction( - action: TypeAgentAction<${pascalName}Actions>, - context: ActionContext, -): Promise { - const { client } = context.sessionContext.agentContext; - // TODO: map each action to a client.callApi() call. - return createActionResultFromTextDisplay( - \`Executing \${action.actionName} — not yet implemented.\`, - ); -} -`; + return loadTemplate("externalApiHandler.ts", { + agentName: name, + AgentName: pascalName, + }); } function buildLlmStreamingHandler(name: string, pascalName: string): string { - return `// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -// Pattern: llm-streaming — LLM-injected agent with streaming responses. -// Runs inside the dispatcher process (injected: true in manifest). -// Uses aiclient + typechat; streams partial results via streamingActionContext. - -import { - ActionContext, - AppAgent, - TypeAgentAction, - ActionResult, -} from "@typeagent/agent-sdk"; -import { createActionResultFromMarkdownDisplay } from "@typeagent/agent-sdk/helpers/action"; -import { ${pascalName}Actions } from "./${name}Schema.js"; - -export function instantiate(): AppAgent { - return { - initializeAgentContext, - executeAction, - }; -} - -async function initializeAgentContext(): Promise { - return {}; -} - -async function executeAction( - action: TypeAgentAction<${pascalName}Actions>, - context: ActionContext, -): Promise { - switch (action.actionName) { - case "generateResponse": { - // TODO: call your LLM and stream chunks via: - // context.streamingActionContext?.appendDisplay(chunk) - return createActionResultFromMarkdownDisplay( - "Streaming response not yet implemented.", - ); - } - default: - return createActionResultFromMarkdownDisplay( - \`Unknown action: \${(action as any).actionName}\`, - ); - } -} -`; + return loadTemplate("llmStreamingHandler.ts", { + agentName: name, + AgentName: pascalName, + }); } function buildSubAgentOrchestratorHandler( name: string, pascalName: string, ): string { - return `// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -// Pattern: sub-agent-orchestrator — root agent routing to N typed sub-schemas. -// Add one executeXxxAction() per sub-schema group defined in subActionManifests. -// The root executeAction routes by action name (each group owns disjoint names). - -import { - ActionContext, - AppAgent, - TypeAgentAction, - ActionResult, -} from "@typeagent/agent-sdk"; -import { createActionResultFromTextDisplay } from "@typeagent/agent-sdk/helpers/action"; -import { ${pascalName}Actions } from "./${name}Schema.js"; - -export function instantiate(): AppAgent { - return { - initializeAgentContext, - executeAction, - }; -} - -async function initializeAgentContext(): Promise { - return {}; -} - -async function executeAction( - action: TypeAgentAction<${pascalName}Actions>, - context: ActionContext, -): Promise { - // TODO: route to sub-schema handlers, e.g.: - // if (isGroupOneAction(action)) return executeGroupOneAction(action, context); - // if (isGroupTwoAction(action)) return executeGroupTwoAction(action, context); - return createActionResultFromTextDisplay( - \`Executing \${action.actionName} — not yet implemented.\`, - ); -} - -// ---- Sub-schema handlers (one per subActionManifests group) ------------ - -// async function executeGroupOneAction( -// action: TypeAgentAction, -// context: ActionContext, -// ): Promise { ... } -`; + return loadTemplate("subAgentOrchestratorHandler.ts", { + agentName: name, + AgentName: pascalName, + }); } function buildWebSocketBridgeHandler(name: string, pascalName: string): string { - return `// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -// Pattern: websocket-bridge — bidirectional RPC to a host-side plugin. -// The agent owns a WebSocketServer; the host plugin connects as the client. -// Commands flow TypeAgent → WebSocket → plugin → response. - -import { - ActionContext, - AppAgent, - SessionContext, - TypeAgentAction, - ActionResult, -} from "@typeagent/agent-sdk"; -import { createActionResultFromTextDisplay } from "@typeagent/agent-sdk/helpers/action"; -import { WebSocketServer, WebSocket } from "ws"; -import { ${pascalName}Actions } from "./${name}Schema.js"; - -const BRIDGE_PORT = 5678; // TODO: choose an unused port - -// ---- WebSocket bridge -------------------------------------------------- - -type BridgeRequest = { id: string; actionName: string; parameters: unknown }; -type BridgeResponse = { id: string; success: boolean; result?: unknown; error?: string }; - -class ${pascalName}Bridge { - private wss: WebSocketServer | undefined; - private client: WebSocket | undefined; - private pending = new Map void>(); - - start(): void { - this.wss = new WebSocketServer({ port: BRIDGE_PORT }); - this.wss.on("connection", (ws) => { - this.client = ws; - ws.on("message", (data) => { - const response = JSON.parse(data.toString()) as BridgeResponse; - this.pending.get(response.id)?.(response); - this.pending.delete(response.id); - }); - ws.on("close", () => { this.client = undefined; }); - }); - } - - async stop(): Promise { - return new Promise((resolve) => this.wss?.close(() => resolve())); - } - - async send(actionName: string, parameters: unknown): Promise { - if (!this.client) { - throw new Error("No host plugin connected on port " + BRIDGE_PORT); - } - const id = \`\${Date.now()}-\${Math.random().toString(36).slice(2)}\`; - return new Promise((resolve, reject) => { - this.pending.set(id, (res) => - res.success ? resolve(res.result) : reject(new Error(res.error)), - ); - this.client!.send( - JSON.stringify({ id, actionName, parameters } satisfies BridgeRequest), - ); - }); - } - - get connected(): boolean { return this.client !== undefined; } -} - -// ---- Agent lifecycle --------------------------------------------------- - -type Context = { bridge: ${pascalName}Bridge }; - -export function instantiate(): AppAgent { - return { - initializeAgentContext, - updateAgentContext, - closeAgentContext, - executeAction, - }; -} - -async function initializeAgentContext(): Promise { - const bridge = new ${pascalName}Bridge(); - bridge.start(); - return { bridge }; -} - -async function updateAgentContext( - _enable: boolean, - _context: SessionContext, - _schemaName: string, -): Promise {} - -async function closeAgentContext(context: SessionContext): Promise { - await context.agentContext.bridge.stop(); -} - -async function executeAction( - action: TypeAgentAction<${pascalName}Actions>, - context: ActionContext, -): Promise { - const { bridge } = context.sessionContext.agentContext; - if (!bridge.connected) { - return { - error: \`Host plugin not connected. Make sure the ${name} plugin is running on port \${BRIDGE_PORT}.\`, - }; - } - try { - const result = await bridge.send(action.actionName, action.parameters); - return createActionResultFromTextDisplay(JSON.stringify(result, null, 2)); - } catch (err: any) { - return { error: err?.message ?? String(err) }; - } -} -`; + const portEnv = `${name.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_BRIDGE_PORT`; + return loadTemplate("websocketBridgeHandler.ts", { + agentName: name, + AgentName: pascalName, + PORT_ENV: portEnv, + }); } function buildStateMachineHandler(name: string, pascalName: string): string { - return `// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -// Pattern: state-machine — multi-phase disk-persisted workflow. -// State is stored in ~/.typeagent/${name}//state.json. -// Each phase must be approved before the next begins. - -import { - ActionContext, - AppAgent, - TypeAgentAction, - ActionResult, -} from "@typeagent/agent-sdk"; -import { createActionResultFromMarkdownDisplay } from "@typeagent/agent-sdk/helpers/action"; -import { ${pascalName}Actions } from "./${name}Schema.js"; -import fs from "fs/promises"; -import path from "path"; -import os from "os"; - -const STATE_ROOT = path.join(os.homedir(), ".typeagent", "${name}"); - -// ---- State types ------------------------------------------------------- - -type PhaseStatus = "pending" | "in-progress" | "approved"; - -type WorkflowState = { - workflowId: string; - currentPhase: string; - phases: Record; - config: Record; - createdAt: string; - updatedAt: string; -}; - -// ---- State I/O --------------------------------------------------------- - -async function loadState(workflowId: string): Promise { - const statePath = path.join(STATE_ROOT, workflowId, "state.json"); - try { - return JSON.parse(await fs.readFile(statePath, "utf-8")) as WorkflowState; - } catch { - return undefined; - } -} - -async function saveState(state: WorkflowState): Promise { - const stateDir = path.join(STATE_ROOT, state.workflowId); - await fs.mkdir(stateDir, { recursive: true }); - state.updatedAt = new Date().toISOString(); - await fs.writeFile( - path.join(stateDir, "state.json"), - JSON.stringify(state, null, 2), - "utf-8", - ); -} - -// ---- Agent lifecycle --------------------------------------------------- - -export function instantiate(): AppAgent { - return { - initializeAgentContext, - executeAction, - }; -} - -async function initializeAgentContext(): Promise { - await fs.mkdir(STATE_ROOT, { recursive: true }); - return {}; -} - -async function executeAction( - action: TypeAgentAction<${pascalName}Actions>, - _context: ActionContext, -): Promise { - // TODO: map actions to phase handlers, e.g.: - // case "startWorkflow": return handleStart(action.parameters.workflowId); - // case "runPhaseOne": return handlePhaseOne(action.parameters.workflowId); - // case "approvePhase": return handleApprove(action.parameters.workflowId, action.parameters.phase); - // case "getStatus": return handleStatus(action.parameters.workflowId); - return createActionResultFromMarkdownDisplay( - \`Executing \${action.actionName} — not yet implemented.\`, - ); -} -`; + return loadTemplate("stateMachineHandler.ts", { + agentName: name, + AgentName: pascalName, + }); } function buildNativePlatformHandler(name: string, pascalName: string): string { - return `// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -// Pattern: native-platform — OS/device APIs via child_process or SDK. -// No cloud dependency. Handle platform differences in executeCommand(). - -import { - ActionContext, - AppAgent, - TypeAgentAction, - ActionResult, -} from "@typeagent/agent-sdk"; -import { createActionResultFromTextDisplay } from "@typeagent/agent-sdk/helpers/action"; -import { exec } from "child_process"; -import { promisify } from "util"; -import { ${pascalName}Actions } from "./${name}Schema.js"; - -const execAsync = promisify(exec); -const platform = process.platform; // "win32" | "darwin" | "linux" - -export function instantiate(): AppAgent { - return { - initializeAgentContext, - executeAction, - }; -} - -async function initializeAgentContext(): Promise { - return {}; -} - -async function executeAction( - action: TypeAgentAction<${pascalName}Actions>, - _context: ActionContext, -): Promise { - try { - const output = await executeCommand( - action.actionName, - action.parameters as Record, - ); - return createActionResultFromTextDisplay(output ?? "Done."); - } catch (err: any) { - return { error: err?.message ?? String(err) }; - } -} - -/** - * Map a typed action to a platform-specific shell command or SDK call. - * Add one case per action defined in ${pascalName}Actions. - */ -async function executeCommand( - actionName: string, - parameters: Record, -): Promise { - switch (actionName) { - // TODO: add cases for each action. Example: - // case "openFile": { - // const cmd = platform === "win32" ? \`start "" "\${parameters.path}"\` - // : platform === "darwin" ? \`open "\${parameters.path}"\` - // : \`xdg-open "\${parameters.path}"\`; - // return (await execAsync(cmd)).stdout; - // } - default: - throw new Error(\`Not implemented: \${actionName}\`); - } -} -`; + return loadTemplate("nativePlatformHandler.ts", { + agentName: name, + AgentName: pascalName, + }); } function buildViewUiHandler(name: string, pascalName: string): string { - return `// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -// Pattern: view-ui — web view renderer with IPC handler. -// Opens a local HTTP server serving site/ and communicates via display APIs. -// The actual UX lives in the site/ directory. - -import { - ActionContext, - AppAgent, - SessionContext, - TypeAgentAction, - ActionResult, -} from "@typeagent/agent-sdk"; -import { createActionResultFromHtmlDisplay } from "@typeagent/agent-sdk/helpers/action"; -import { ${pascalName}Actions } from "./${name}Schema.js"; - -const VIEW_PORT = 3456; // TODO: choose an unused port - -export function instantiate(): AppAgent { - return { - initializeAgentContext, - updateAgentContext, - closeAgentContext, - executeAction, - }; -} - -async function initializeAgentContext(): Promise { - // TODO: start the local HTTP server that serves site/ - return {}; -} - -async function updateAgentContext( - enable: boolean, - context: SessionContext, - _schemaName: string, -): Promise { - if (enable) { - await context.agentIO.openLocalView( - context.requestId, - VIEW_PORT, - ); - } else { - await context.agentIO.closeLocalView( - context.requestId, - VIEW_PORT, - ); - } -} - -async function closeAgentContext(_context: SessionContext): Promise { - // TODO: stop the local HTTP server -} - -async function executeAction( - action: TypeAgentAction<${pascalName}Actions>, - _context: ActionContext, -): Promise { - // Push state changes to the view via HTML display updates. - return createActionResultFromHtmlDisplay( - \`

Executing \${action.actionName} — not yet implemented.

\`, - ); -} -`; + const portEnv = `${name.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_VIEW_PORT`; + return loadTemplate("viewUiHandler.ts", { + agentName: name, + AgentName: pascalName, + PORT_ENV: portEnv, + }); } function buildCommandHandlerTemplate(name: string, pascalName: string): string { - return `// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -// Pattern: command-handler — direct dispatch via a handlers map. -// Suited for settings-style agents with a small number of well-known commands. - -import { AppAgent, ActionResult } from "@typeagent/agent-sdk"; -import { createActionResultFromTextDisplay } from "@typeagent/agent-sdk/helpers/action"; - -export function instantiate(): AppAgent { - return getCommandInterface(handlers); -} - -// ---- Handlers ---------------------------------------------------------- -// Add one entry per action name defined in ${pascalName}Actions. - -const handlers: Record Promise> = { - // exampleAction: async (params) => { - // return createActionResultFromTextDisplay("Done."); - // }, -}; - -function getCommandInterface( - handlerMap: Record Promise>, -): AppAgent { - return { - async executeAction(action: any): Promise { - const handler = handlerMap[action.actionName]; - if (!handler) { - return { error: \`Unknown action: \${action.actionName}\` }; - } - return handler(action.parameters); - }, - }; -} -`; + return loadTemplate("commandHandlerTemplate.ts", { + AgentName: pascalName, + }); } diff --git a/ts/packages/agents/onboarding/src/scaffolder/templateLoader.ts b/ts/packages/agents/onboarding/src/scaffolder/templateLoader.ts new file mode 100644 index 0000000000..5567452d1d --- /dev/null +++ b/ts/packages/agents/onboarding/src/scaffolder/templateLoader.ts @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Shared loader for the scaffolder's emitted-code templates. +// +// Templates live in `src/scaffolder/templates/*.ts` so a reviewer can +// read them as plain TypeScript (with syntax highlighting and +// `tsc`-level verification — see `__agentName__Schema.ts` for the +// type-check stub) instead of wading through 200-line template +// literals inside scaffolderHandler.ts. +// +// Placeholders use the `__TOKEN__` convention — chosen so the templates +// remain valid TypeScript identifiers (`class __AgentName__Bridge {}`, +// `process.env["__PORT_ENV__"]`, etc.). The substitution regex only +// matches identifiers wrapped in *paired* double-underscores, so +// Node's `__filename` / `__dirname` (single trailing underscore is +// absent) are left untouched. +// +// Templates are loaded at scaffold time (once per generated file) so the +// sync I/O cost is negligible and lets build* helpers stay synchronous — +// keeping their call sites unchanged. + +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// At runtime __dirname is `dist/scaffolder/`. Templates ship only in +// `src/` (this package isn't published to npm and the postbuild copies +// schema artifacts, not these). Resolve back to `src/scaffolder/templates`. +function templatePath(filename: string): string { + return path.resolve(__dirname, "../../src/scaffolder/templates", filename); +} + +// Files in the templates directory that are type-check scaffolding only +// (stubs for placeholder identifiers) and must never be loaded as a +// template at scaffold time. +const RESERVED_TEMPLATE_NAMES = new Set(["__agentName__Schema.ts"]); + +/** + * Load `filename` from `src/scaffolder/templates/` and substitute every + * `__TOKEN__` with `vars[TOKEN]`. Throws if any `__TOKEN__` placeholder + * remains after substitution — that catches typos in either the + * template or the caller's `vars` map at scaffold time rather than + * emitting broken code. + * + * For TypeScript templates the recommended `vars` keys are: + * - `agentName` (camelCase agent name) + * - `AgentName` (PascalCase agent name) + * - `PORT_ENV` (uppercase env-var name) + * - `BRIDGE_PORT` (numeric default port literal) + * + * Non-TS templates (`.template` extension) use the same `__TOKEN__` + * convention as well. + */ +export function loadTemplate( + filename: string, + vars: Record, +): string { + if (RESERVED_TEMPLATE_NAMES.has(filename)) { + throw new Error( + `Template ${filename} is a type-check stub and cannot be loaded.`, + ); + } + const tpl = fs.readFileSync(templatePath(filename), "utf-8"); + // Single-pass regex replacement so a substituted value that happens + // to contain a `__KEY__` token is NOT re-processed by a later + // iteration -- important if a future caller ever passes a var + // derived from user input. Unknown placeholders are left in place + // and surfaced by the leftover check below. + // + // Requires *paired* leading and trailing double-underscores: matches + // `__agentName__` but leaves Node's `__filename` (no trailing `__`) + // and `____` (empty body) alone. + const out = tpl.replace(/__([A-Za-z_][A-Za-z0-9_]*)__/g, (match, key) => + key in vars ? vars[key] : match, + ); + const leftover = out.match(/__[A-Za-z_][A-Za-z0-9_]*__/g); + if (leftover && leftover.length > 0) { + const unique = Array.from(new Set(leftover)).join(", "); + throw new Error( + `Template ${filename} has unsubstituted placeholders: ${unique}`, + ); + } + return out; +} diff --git a/ts/packages/agents/onboarding/src/scaffolder/templates/__agentName__Schema.ts b/ts/packages/agents/onboarding/src/scaffolder/templates/__agentName__Schema.ts new file mode 100644 index 0000000000..5b7da84d2d --- /dev/null +++ b/ts/packages/agents/onboarding/src/scaffolder/templates/__agentName__Schema.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// SCAFFOLDER TEMPLATE STUB — DO NOT IMPORT AT RUNTIME. +// +// This file is a placeholder schema module so the sibling `*.ts` scaffolder +// templates in this directory can be type-checked standalone by `tsc`. The +// real generated agent ships its own `Schema.ts` next to its handler; +// at scaffold time `templateLoader` rewrites the import path so this stub +// is never referenced by emitted code. + +// eslint-disable-next-line @typescript-eslint/naming-convention +export type __AgentName__Actions = { + actionName: string; + parameters?: Record; +}; diff --git a/ts/packages/agents/onboarding/src/scaffolder/templates/commandHandlerTemplate.ts b/ts/packages/agents/onboarding/src/scaffolder/templates/commandHandlerTemplate.ts new file mode 100644 index 0000000000..dc7dd75a5b --- /dev/null +++ b/ts/packages/agents/onboarding/src/scaffolder/templates/commandHandlerTemplate.ts @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Pattern: command-handler — direct dispatch via a handlers map. +// Suited for settings-style agents with a small number of well-known commands. + +import { AppAgent, ActionResult } from "@typeagent/agent-sdk"; +import { createActionResultFromTextDisplay } from "@typeagent/agent-sdk/helpers/action"; + +export function instantiate(): AppAgent { + return getCommandInterface(handlers); +} + +// ---- Handlers ---------------------------------------------------------- +// Add one entry per action name defined in __AgentName__Actions. + +const handlers: Record Promise> = { + // exampleAction: async (params) => { + // return createActionResultFromTextDisplay("Done."); + // }, +}; + +function getCommandInterface( + handlerMap: Record Promise>, +): AppAgent { + return { + async executeAction(action: any): Promise { + const handler = handlerMap[action.actionName]; + if (!handler) { + return { error: `Unknown action: ${action.actionName}` }; + } + return handler(action.parameters); + }, + }; +} diff --git a/ts/packages/agents/onboarding/src/scaffolder/templates/externalApiHandler.ts b/ts/packages/agents/onboarding/src/scaffolder/templates/externalApiHandler.ts new file mode 100644 index 0000000000..62df2a7e45 --- /dev/null +++ b/ts/packages/agents/onboarding/src/scaffolder/templates/externalApiHandler.ts @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Pattern: external-api — REST/OAuth cloud API bridge. +// Implement __AgentName__Client with your API's authentication and endpoints. + +import { + ActionContext, + AppAgent, + SessionContext, + TypeAgentAction, + ActionResult, +} from "@typeagent/agent-sdk"; +import { createActionResultFromTextDisplay } from "@typeagent/agent-sdk/helpers/action"; +import { __AgentName__Actions } from "./__agentName__Schema.js"; + +// ---- API client -------------------------------------------------------- + +class __AgentName__Client { + private token: string | undefined; + + /** Authenticate and store the access token. */ + async authenticate(): Promise { + // TODO: implement OAuth flow or API key loading. + // Store token in: ~/.typeagent/profiles//__agentName__/token.json + throw new Error("authenticate() not yet implemented"); + } + + async callApi( + endpoint: string, + params: Record, + ): Promise { + if (!this.token) await this.authenticate(); + // TODO: implement HTTP call using this.token + throw new Error(`callApi(${endpoint}) not yet implemented`); + } +} + +// ---- Agent lifecycle --------------------------------------------------- + +type Context = { client: __AgentName__Client }; + +export function instantiate(): AppAgent { + return { + initializeAgentContext, + updateAgentContext, + executeAction, + }; +} + +async function initializeAgentContext(): Promise { + return { client: new __AgentName__Client() }; +} + +async function updateAgentContext( + _enable: boolean, + _context: SessionContext, + _schemaName: string, +): Promise { + // Optionally authenticate eagerly when the agent is enabled. +} + +async function executeAction( + action: TypeAgentAction<__AgentName__Actions>, + context: ActionContext, +): Promise { + const { client } = context.sessionContext.agentContext; + // TODO: map each action to a client.callApi() call. + return createActionResultFromTextDisplay( + `Executing ${action.actionName} — not yet implemented.`, + ); +} diff --git a/ts/packages/agents/onboarding/src/scaffolder/templates/llmStreamingHandler.ts b/ts/packages/agents/onboarding/src/scaffolder/templates/llmStreamingHandler.ts new file mode 100644 index 0000000000..66c41454b4 --- /dev/null +++ b/ts/packages/agents/onboarding/src/scaffolder/templates/llmStreamingHandler.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Pattern: llm-streaming — LLM-injected agent with streaming responses. +// Runs inside the dispatcher process (injected: true in manifest). +// Uses aiclient + typechat; streams partial results via streamingActionContext. + +import { + ActionContext, + AppAgent, + TypeAgentAction, + ActionResult, +} from "@typeagent/agent-sdk"; +import { createActionResultFromMarkdownDisplay } from "@typeagent/agent-sdk/helpers/action"; +import { __AgentName__Actions } from "./__agentName__Schema.js"; + +export function instantiate(): AppAgent { + return { + initializeAgentContext, + executeAction, + }; +} + +async function initializeAgentContext(): Promise { + return {}; +} + +async function executeAction( + action: TypeAgentAction<__AgentName__Actions>, + context: ActionContext, +): Promise { + switch (action.actionName) { + case "generateResponse": { + // TODO: call your LLM and stream chunks via: + // context.streamingActionContext?.appendDisplay(chunk) + return createActionResultFromMarkdownDisplay( + "Streaming response not yet implemented.", + ); + } + default: + return createActionResultFromMarkdownDisplay( + `Unknown action: ${(action as any).actionName}`, + ); + } +} diff --git a/ts/packages/agents/onboarding/src/scaffolder/templates/nativePlatformHandler.ts b/ts/packages/agents/onboarding/src/scaffolder/templates/nativePlatformHandler.ts new file mode 100644 index 0000000000..531cccad07 --- /dev/null +++ b/ts/packages/agents/onboarding/src/scaffolder/templates/nativePlatformHandler.ts @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Pattern: native-platform — OS/device APIs via child_process or SDK. +// No cloud dependency. Handle platform differences in executeCommand(). + +import { + ActionContext, + AppAgent, + TypeAgentAction, + ActionResult, +} from "@typeagent/agent-sdk"; +import { createActionResultFromTextDisplay } from "@typeagent/agent-sdk/helpers/action"; +import { exec } from "child_process"; +import { promisify } from "util"; +import { __AgentName__Actions } from "./__agentName__Schema.js"; + +const execAsync = promisify(exec); +const platform = process.platform; // "win32" | "darwin" | "linux" + +export function instantiate(): AppAgent { + return { + initializeAgentContext, + executeAction, + }; +} + +async function initializeAgentContext(): Promise { + return {}; +} + +async function executeAction( + action: TypeAgentAction<__AgentName__Actions>, + _context: ActionContext, +): Promise { + try { + const output = await executeCommand( + action.actionName, + action.parameters as Record, + ); + return createActionResultFromTextDisplay(output ?? "Done."); + } catch (err: any) { + return { error: err?.message ?? String(err) }; + } +} + +/** + * Map a typed action to a platform-specific shell command or SDK call. + * Add one case per action defined in __AgentName__Actions. + */ +async function executeCommand( + actionName: string, + parameters: Record, +): Promise { + switch (actionName) { + // TODO: add cases for each action. Example: + // case "openFile": { + // const cmd = platform === "win32" ? `start "" "${parameters.path}"` + // : platform === "darwin" ? `open "${parameters.path}"` + // : `xdg-open "${parameters.path}"`; + // return (await execAsync(cmd)).stdout; + // } + default: + throw new Error(`Not implemented: ${actionName}`); + } +} diff --git a/ts/packages/agents/onboarding/src/scaffolder/templates/officeAddinHtml.template b/ts/packages/agents/onboarding/src/scaffolder/templates/officeAddinHtml.template new file mode 100644 index 0000000000..d0a92d4d6b --- /dev/null +++ b/ts/packages/agents/onboarding/src/scaffolder/templates/officeAddinHtml.template @@ -0,0 +1,13 @@ + + + + + __AgentName__ TypeAgent Add-in + + + + +

__AgentName__ TypeAgent

+
Connecting...
+ + diff --git a/ts/packages/agents/onboarding/src/scaffolder/templates/officeAddinTs.template b/ts/packages/agents/onboarding/src/scaffolder/templates/officeAddinTs.template new file mode 100644 index 0000000000..6c6fdc8e64 --- /dev/null +++ b/ts/packages/agents/onboarding/src/scaffolder/templates/officeAddinTs.template @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Office.js task pane add-in for __agentName__ TypeAgent integration. +// Connects to the TypeAgent bridge via WebSocket and forwards commands +// to the Office.js API. + +const BRIDGE_PORT = __BRIDGE_PORT__; + +Office.onReady(async () => { + document.getElementById("status")!.textContent = "Connecting to TypeAgent..."; + const ws = new WebSocket(`ws://localhost:${BRIDGE_PORT}`); + + ws.onopen = () => { + document.getElementById("status")!.textContent = "Connected"; + ws.send(JSON.stringify({ type: "hello", addinName: "__agentName__" })); + }; + + ws.onmessage = async (event) => { + const command = JSON.parse(event.data); + try { + const result = await executeCommand(command.actionName, command.parameters); + ws.send(JSON.stringify({ id: command.id, success: true, result })); + } catch (err: any) { + ws.send(JSON.stringify({ id: command.id, success: false, error: err?.message ?? String(err) })); + } + }; +}); + +async function executeCommand(actionName: string, parameters: Record): Promise { + // TODO: map actionName to Office.js API calls + throw new Error(`Not implemented: ${actionName}`); +} diff --git a/ts/packages/agents/onboarding/src/scaffolder/templates/officeManifestXml.template b/ts/packages/agents/onboarding/src/scaffolder/templates/officeManifestXml.template new file mode 100644 index 0000000000..b9ace78701 --- /dev/null +++ b/ts/packages/agents/onboarding/src/scaffolder/templates/officeManifestXml.template @@ -0,0 +1,18 @@ + + + + 1.0.0.0 + Microsoft + en-US + + + + + + + + + ReadWriteDocument + diff --git a/ts/packages/agents/onboarding/src/scaffolder/templates/restClientTemplate.ts b/ts/packages/agents/onboarding/src/scaffolder/templates/restClientTemplate.ts new file mode 100644 index 0000000000..8a640cdb6c --- /dev/null +++ b/ts/packages/agents/onboarding/src/scaffolder/templates/restClientTemplate.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// REST client bridge for __agentName__. +// Calls the target API and returns results to the TypeAgent handler. + +export class __AgentName__Bridge { + constructor( + private readonly baseUrl: string, + private readonly apiKey?: string, + ) {} + + async executeCommand( + actionName: string, + parameters: Record, + ): Promise { + // TODO: map actionName to HTTP endpoint and method + throw new Error(`Not implemented: ${actionName}`); + } + + private get headers(): Record { + const h: Record = { + "Content-Type": "application/json", + }; + if (this.apiKey) h["Authorization"] = `Bearer ${this.apiKey}`; + return h; + } +} diff --git a/ts/packages/agents/onboarding/src/scaffolder/templates/schemaGrammarHandler.ts b/ts/packages/agents/onboarding/src/scaffolder/templates/schemaGrammarHandler.ts new file mode 100644 index 0000000000..941eff211d --- /dev/null +++ b/ts/packages/agents/onboarding/src/scaffolder/templates/schemaGrammarHandler.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { + ActionContext, + AppAgent, + TypeAgentAction, + ActionResult, +} from "@typeagent/agent-sdk"; +import { createActionResultFromTextDisplay } from "@typeagent/agent-sdk/helpers/action"; +import { __AgentName__Actions } from "./__agentName__Schema.js"; + +export function instantiate(): AppAgent { + return { + initializeAgentContext, + executeAction, + }; +} + +async function initializeAgentContext(): Promise { + return {}; +} + +async function executeAction( + action: TypeAgentAction<__AgentName__Actions>, + context: ActionContext, +): Promise { + // TODO: implement action handlers + return createActionResultFromTextDisplay( + `Executing ${action.actionName} — not yet implemented.`, + ); +} diff --git a/ts/packages/agents/onboarding/src/scaffolder/templates/stateMachineHandler.ts b/ts/packages/agents/onboarding/src/scaffolder/templates/stateMachineHandler.ts new file mode 100644 index 0000000000..101184390e --- /dev/null +++ b/ts/packages/agents/onboarding/src/scaffolder/templates/stateMachineHandler.ts @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Pattern: state-machine — multi-phase disk-persisted workflow. +// State is stored in ~/.typeagent/__agentName__//state.json. +// Each phase must be approved before the next begins. + +import { + ActionContext, + AppAgent, + TypeAgentAction, + ActionResult, +} from "@typeagent/agent-sdk"; +import { createActionResultFromMarkdownDisplay } from "@typeagent/agent-sdk/helpers/action"; +import { __AgentName__Actions } from "./__agentName__Schema.js"; +import fs from "fs/promises"; +import path from "path"; +import os from "os"; + +const STATE_ROOT = path.join(os.homedir(), ".typeagent", "__agentName__"); + +// ---- State types ------------------------------------------------------- + +type PhaseStatus = "pending" | "in-progress" | "approved"; + +type WorkflowState = { + workflowId: string; + currentPhase: string; + phases: Record; + config: Record; + createdAt: string; + updatedAt: string; +}; + +// ---- State I/O --------------------------------------------------------- + +async function loadState( + workflowId: string, +): Promise { + const statePath = path.join(STATE_ROOT, workflowId, "state.json"); + try { + return JSON.parse( + await fs.readFile(statePath, "utf-8"), + ) as WorkflowState; + } catch { + return undefined; + } +} + +async function saveState(state: WorkflowState): Promise { + const stateDir = path.join(STATE_ROOT, state.workflowId); + await fs.mkdir(stateDir, { recursive: true }); + state.updatedAt = new Date().toISOString(); + await fs.writeFile( + path.join(stateDir, "state.json"), + JSON.stringify(state, null, 2), + "utf-8", + ); +} + +// ---- Agent lifecycle --------------------------------------------------- + +export function instantiate(): AppAgent { + return { + initializeAgentContext, + executeAction, + }; +} + +async function initializeAgentContext(): Promise { + await fs.mkdir(STATE_ROOT, { recursive: true }); + return {}; +} + +async function executeAction( + action: TypeAgentAction<__AgentName__Actions>, + _context: ActionContext, +): Promise { + // TODO: map actions to phase handlers, e.g.: + // case "startWorkflow": return handleStart(action.parameters.workflowId); + // case "runPhaseOne": return handlePhaseOne(action.parameters.workflowId); + // case "approvePhase": return handleApprove(action.parameters.workflowId, action.parameters.phase); + // case "getStatus": return handleStatus(action.parameters.workflowId); + return createActionResultFromMarkdownDisplay( + `Executing ${action.actionName} — not yet implemented.`, + ); +} diff --git a/ts/packages/agents/onboarding/src/scaffolder/templates/subAgentOrchestratorHandler.ts b/ts/packages/agents/onboarding/src/scaffolder/templates/subAgentOrchestratorHandler.ts new file mode 100644 index 0000000000..d62aa44ce5 --- /dev/null +++ b/ts/packages/agents/onboarding/src/scaffolder/templates/subAgentOrchestratorHandler.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Pattern: sub-agent-orchestrator — root agent routing to N typed sub-schemas. +// Add one executeXxxAction() per sub-schema group defined in subActionManifests. +// The root executeAction routes by action name (each group owns disjoint names). + +import { + ActionContext, + AppAgent, + TypeAgentAction, + ActionResult, +} from "@typeagent/agent-sdk"; +import { createActionResultFromTextDisplay } from "@typeagent/agent-sdk/helpers/action"; +import { __AgentName__Actions } from "./__agentName__Schema.js"; + +export function instantiate(): AppAgent { + return { + initializeAgentContext, + executeAction, + }; +} + +async function initializeAgentContext(): Promise { + return {}; +} + +async function executeAction( + action: TypeAgentAction<__AgentName__Actions>, + context: ActionContext, +): Promise { + // TODO: route to sub-schema handlers, e.g.: + // if (isGroupOneAction(action)) return executeGroupOneAction(action, context); + // if (isGroupTwoAction(action)) return executeGroupTwoAction(action, context); + return createActionResultFromTextDisplay( + `Executing ${action.actionName} — not yet implemented.`, + ); +} + +// ---- Sub-schema handlers (one per subActionManifests group) ------------ + +// async function executeGroupOneAction( +// action: TypeAgentAction, +// context: ActionContext, +// ): Promise { ... } diff --git a/ts/packages/agents/onboarding/src/scaffolder/templates/tsconfig.json b/ts/packages/agents/onboarding/src/scaffolder/templates/tsconfig.json new file mode 100644 index 0000000000..463b9c12f2 --- /dev/null +++ b/ts/packages/agents/onboarding/src/scaffolder/templates/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "rootDir": ".", + "outDir": "../../../dist/scaffolder/templates", + "noUnusedLocals": false, + "noUnusedParameters": false + }, + "include": ["./**/*"] +} diff --git a/ts/packages/agents/onboarding/src/scaffolder/templates/viewUiHandler.ts b/ts/packages/agents/onboarding/src/scaffolder/templates/viewUiHandler.ts new file mode 100644 index 0000000000..93a6229a15 --- /dev/null +++ b/ts/packages/agents/onboarding/src/scaffolder/templates/viewUiHandler.ts @@ -0,0 +1,225 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Pattern: view-ui — web view renderer with IPC handler. +// Opens a local HTTP server serving site/ and surfaces it in the shell +// via an ActivityContext with openLocalView=true. +// +// Port allocation: the view server binds on an OS-assigned ephemeral +// port (port=0) by default. The actual port is registered with the +// dispatcher via context.registerPort("view", port) so external +// clients can discover it through the agent-server's discovery channel +// (discoverPort("__agentName__", "view")). context.setLocalHostPort(port) is +// also called so the embedding shell knows which port to load when an +// action returns openLocalView=true. Set __PORT_ENV__ to pin the view +// to a fixed port when debugging. + +import { + ActionContext, + ActionResult, + ActivityContext, + AppAgent, + SessionContext, + TypeAgentAction, +} from "@typeagent/agent-sdk"; +import { + createActionResult, + createActionResultFromHtmlDisplay, +} from "@typeagent/agent-sdk/helpers/action"; +import { createServer, Server } from "node:http"; +import { AddressInfo } from "node:net"; +import { __AgentName__Actions } from "./__agentName__Schema.js"; + +type __AgentName__AgentContext = { + server?: Server; + port?: number; + portRegistration?: { release: () => void }; +}; + +function getViewBindPort(): number { + const v = process.env["__PORT_ENV__"]; + if (!v) return 0; + const n = parseInt(v, 10); + return Number.isFinite(n) && n >= 0 ? n : 0; +} + +export function instantiate(): AppAgent { + return { + initializeAgentContext, + updateAgentContext, + closeAgentContext, + executeAction, + }; +} + +async function initializeAgentContext(): Promise<__AgentName__AgentContext> { + return {}; +} + +/** + * Bind the view server on `port` (0 = OS-assigned). Returns the actual + * bound port so it can be registered and surfaced to the shell. + * Rejects on bind failure (EADDRINUSE under a fixed-port override) so + * callers see the problem instead of having it swallowed by a late + * error handler. + */ +function startViewServer( + port: number, +): Promise<{ server: Server; port: number }> { + return new Promise((resolve, reject) => { + const server = createServer((req, res) => { + // TODO: serve static assets from ./site/, plus any + // JSON/IPC endpoints the view needs. For now, a placeholder. + res.writeHead(200, { "Content-Type": "text/html" }); + // Escape req.url before echoing it into HTML — it is attacker- + // controlled and would otherwise be a reflected XSS sink. + const safePath = String(req.url ?? "/").replace( + /[&<>"']/g, + (c) => + ({ + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + })[c] as string, + ); + res.end(`

__AgentName__ view

Path: ${safePath}

`); + }); + let settled = false; + const onError = (e: Error) => { + if (settled) return; + settled = true; + server.removeListener("listening", onListening); + reject(e); + }; + const onListening = () => { + if (settled) return; + settled = true; + server.removeListener("error", onError); + const addr = server.address() as AddressInfo | null; + if (!addr || typeof addr === "string") { + server.close(); + reject( + new Error( + "http server.address() did not return AddressInfo", + ), + ); + return; + } + // Re-attach a permanent error handler so post-listen errors + // are logged rather than crashing the process. + server.on("error", (err) => { + console.error( + `[__agentName__ view] post-listen http server error: ${err.message}`, + ); + }); + resolve({ server, port: addr.port }); + }; + server.once("error", onError); + server.once("listening", onListening); + server.listen(port); + }); +} + +async function updateAgentContext( + enable: boolean, + context: SessionContext<__AgentName__AgentContext>, + _schemaName: string, +): Promise { + const agentContext = context.agentContext; + if (enable) { + if (agentContext.server !== undefined) { + // Already bound for this session. + return; + } + const { server, port } = await startViewServer(getViewBindPort()); + try { + agentContext.server = server; + agentContext.port = port; + agentContext.portRegistration = context.registerPort("view", port); + // Tell the embedding shell which port to load when an + // action returns openLocalView=true. Goes through the + // registrar with role="default", so the discovery-channel + // role "view" above keeps a stable contract for out-of- + // process clients regardless of this back-compat call. + context.setLocalHostPort(port); + } catch (e) { + // Roll back if registration/setLocalHostPort fails so a + // retry sees a clean slate. + agentContext.portRegistration?.release(); + await new Promise((resolve) => server.close(() => resolve())); + delete agentContext.server; + delete agentContext.port; + delete agentContext.portRegistration; + throw e; + } + } else { + if (agentContext.server === undefined) return; + agentContext.portRegistration?.release(); + delete agentContext.portRegistration; + const server = agentContext.server; + delete agentContext.server; + delete agentContext.port; + // Resolve when the server has fully released its port — + // important for a rapid disable→enable cycle under a fixed- + // port override (`__PORT_ENV__`), where a synchronous return + // would race the new bind into EADDRINUSE. + await new Promise((resolve) => server.close(() => resolve())); + } +} + +async function closeAgentContext( + context: SessionContext<__AgentName__AgentContext>, +): Promise { + // Backstop: if updateAgentContext(false) wasn't called (e.g. crash + // during shutdown), release the registration and close the server + // so the port doesn't leak. + const agentContext = context.agentContext; + agentContext.portRegistration?.release(); + delete agentContext.portRegistration; + if (agentContext.server) { + const server = agentContext.server; + delete agentContext.server; + delete agentContext.port; + await new Promise((resolve) => server.close(() => resolve())); + } +} + +async function executeAction( + action: TypeAgentAction<__AgentName__Actions>, + context: ActionContext<__AgentName__AgentContext>, +): Promise { + const port = context.sessionContext.agentContext.port; + // Returning an ActivityContext with openLocalView=true signals the + // shell to open the local view (it uses the port published via + // setLocalHostPort during enable). Drop the activityContext field + // if your action doesn't need to surface the view. + const activityContext: ActivityContext | undefined = + port !== undefined + ? { + appAgentName: "__agentName__", + activityName: action.actionName, + description: `__AgentName__: ${action.actionName}`, + state: {}, + openLocalView: true, + } + : undefined; + const result = createActionResultFromHtmlDisplay( + `

Executing ${action.actionName} — not yet implemented.

`, + ); + if (activityContext) { + // ActivityContext is attached so the shell can open the view. + // The shape comes from the SDK; cast through unknown to keep + // the template free of internal-only ActionResult fields. + ( + result as unknown as { activityContext: ActivityContext } + ).activityContext = activityContext; + } + return result; +} + +// Silence unused-import warning when the action handler is stripped +// down. `createActionResult` is provided alongside the HTML helper for +// callers that want a richer entity-bearing result. +void createActionResult; diff --git a/ts/packages/agents/onboarding/src/scaffolder/templates/websocketBridgeHandler.ts b/ts/packages/agents/onboarding/src/scaffolder/templates/websocketBridgeHandler.ts new file mode 100644 index 0000000000..18b8a87a5f --- /dev/null +++ b/ts/packages/agents/onboarding/src/scaffolder/templates/websocketBridgeHandler.ts @@ -0,0 +1,409 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Pattern: websocket-bridge — bidirectional RPC to a host-side plugin. +// The agent owns a WebSocketServer; the host plugin connects as the client. +// Commands flow TypeAgent → WebSocket → plugin → response. +// +// Port allocation: the bridge binds on an OS-assigned ephemeral port +// (port=0) by default. The actual port is registered with the dispatcher +// via context.registerPort("default", port) so external clients can +// discover it through the agent-server's discovery channel +// (discoverPort("__agentName__", "default")). Set __PORT_ENV__ to pin the +// bridge to a fixed port when debugging or when a host plugin expects +// a known address. +// +// Lifecycle: one bridge per process, refcounted across enabled sessions. +// Each enabled session registers the bridge under its own +// sessionContextId; lookup("__agentName__", "default") keeps returning the +// port as long as ≥1 session has the agent enabled. The dispatcher's +// closeSessionContext backstop releases stale per-session registrations +// if disable is skipped (e.g. crash). + +import { + ActionContext, + AppAgent, + SessionContext, + TypeAgentAction, + ActionResult, +} from "@typeagent/agent-sdk"; +import { createActionResultFromTextDisplay } from "@typeagent/agent-sdk/helpers/action"; +import { WebSocketServer, WebSocket } from "ws"; +import { AddressInfo } from "net"; +import { __AgentName__Actions } from "./__agentName__Schema.js"; + +function getBridgeBindPort(): number { + const v = process.env["__PORT_ENV__"]; + if (!v) return 0; + const n = parseInt(v, 10); + return Number.isFinite(n) && n >= 0 ? n : 0; +} + +// ---- WebSocket bridge -------------------------------------------------- + +type BridgeRequest = { id: string; actionName: string; parameters: unknown }; +type BridgeResponse = { + id: string; + success: boolean; + result?: unknown; + error?: string; +}; + +class __AgentName__Bridge { + private clients = new Map(); + private nextClientId = 0; + private pending = new Map< + string, + { + resolve: (result: unknown) => void; + reject: (err: Error) => void; + } + >(); + + // Construction is private — use {@link __AgentName__Bridge.start} so + // callers always get a bridge that is guaranteed to be bound before + // they read {@link port} or pass it to the registrar. + private constructor( + private readonly server: WebSocketServer, + public readonly port: number, + ) { + this.server.on("connection", (ws) => { + const id = `c-${++this.nextClientId}`; + this.clients.set(id, ws); + ws.on("message", (data) => { + try { + const response = JSON.parse( + data.toString(), + ) as BridgeResponse; + const entry = this.pending.get(response.id); + if (entry) { + this.pending.delete(response.id); + if (response.success) entry.resolve(response.result); + else entry.reject(new Error(response.error)); + } + } catch { + // Ignore malformed payloads. + } + }); + // Reject any pending requests routed through the bridge when + // the last connected client drops; without this, in-flight + // callers hang until the bridge is closed. + const onDisconnect = () => { + this.clients.delete(id); + if (this.clients.size === 0 && this.pending.size > 0) { + const err = new Error( + "Host plugin disconnected before responding.", + ); + for (const entry of this.pending.values()) { + entry.reject(err); + } + this.pending.clear(); + } + }; + ws.on("close", onDisconnect); + ws.on("error", onDisconnect); + }); + } + + /** + * Bind a new bridge on `port`. Pass 0 (default) to let the OS pick a + * free ephemeral port; read the actual bound port from {@link port} + * after the returned promise resolves. Rejects on bind failure + * (EADDRINUSE under a fixed-port override) so callers see the + * problem instead of having it swallowed by a late error handler. + */ + public static start(port: number = 0): Promise<__AgentName__Bridge> { + return new Promise((resolve, reject) => { + const server = new WebSocketServer({ port }); + let settled = false; + const onError = (e: Error) => { + if (settled) return; + settled = true; + server.removeListener("listening", onListening); + reject(e); + }; + const onListening = () => { + if (settled) return; + settled = true; + server.removeListener("error", onError); + const addr = server.address() as AddressInfo | null; + if (!addr || typeof addr === "string") { + server.close(); + reject( + new Error( + "ws server.address() did not return AddressInfo", + ), + ); + return; + } + // Re-attach a permanent error handler so post-listen errors + // are surfaced rather than crashing the process. + server.on("error", (err) => { + console.error( + `[__agentName__Bridge] post-listen server error: ${err.message}`, + ); + }); + resolve(new __AgentName__Bridge(server, addr.port)); + }; + server.once("error", onError); + server.once("listening", onListening); + }); + } + + /** + * Close all client connections and the underlying server. Pending + * `send` promises are rejected so callers never hang on a closed + * bridge. Resolves when the server has fully released its port — + * important for a rapid disable→enable cycle under a fixed-port + * override (`__PORT_ENV__`), where a synchronous return would race + * the new bind into EADDRINUSE. + */ + public close(): Promise { + const closedError = new Error( + "__AgentName__Bridge closed before response was received.", + ); + for (const entry of this.pending.values()) { + entry.reject(closedError); + } + this.pending.clear(); + for (const c of this.clients.values()) { + if (c.readyState === WebSocket.OPEN) c.close(); + } + this.clients.clear(); + return new Promise((resolve) => this.server.close(() => resolve())); + } + + public get connected(): boolean { + for (const c of this.clients.values()) { + if (c.readyState === WebSocket.OPEN) return true; + } + return false; + } + + public async send( + actionName: string, + parameters: unknown, + ): Promise { + // Use the first OPEN client (single-plugin pattern). Adapt this + // selection if you need fan-out or per-session client targeting. + let target: WebSocket | undefined; + for (const c of this.clients.values()) { + if (c.readyState === WebSocket.OPEN) { + target = c; + break; + } + } + if (!target) { + throw new Error( + "No host plugin connected to the __agentName__ bridge.", + ); + } + const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`; + return new Promise((resolve, reject) => { + this.pending.set(id, { resolve, reject }); + target!.send( + JSON.stringify({ + id, + actionName, + parameters, + } satisfies BridgeRequest), + (err) => { + // ws.send errors surface here; without this, a send + // failure (socket closed between readyState check and + // send) would leak the pending entry and hang the + // caller. + if (err) { + this.pending.delete(id); + reject(err); + } + }, + ); + }); + } +} + +// ---- Shared module state ----------------------------------------------- +// +// Storing the bridge per-session would cause "no connection" errors when +// an action runs on a session different from the one that started the +// server, and would mask EADDRINUSE failures from a second bind under a +// fixed-port override. The shared-bridge + per-session-registration +// pattern matches the code and browser agents. + +let sharedBridge: __AgentName__Bridge | undefined; +let sharedStartingPromise: Promise<__AgentName__Bridge> | undefined; +let sharedClosingPromise: Promise | undefined; +let sharedRefCount = 0; + +// Serialize concurrent starts; await any in-flight close before binding +// again so a rapid disable→enable doesn't race the port release. +async function ensureSharedBridge(): Promise<__AgentName__Bridge> { + if (sharedClosingPromise !== undefined) { + await sharedClosingPromise; + } + if (sharedBridge !== undefined) return sharedBridge; + if (sharedStartingPromise !== undefined) return sharedStartingPromise; + sharedStartingPromise = (async () => { + try { + sharedBridge = await __AgentName__Bridge.start(getBridgeBindPort()); + return sharedBridge; + } finally { + sharedStartingPromise = undefined; + } + })(); + return sharedStartingPromise; +} + +// ---- Agent lifecycle --------------------------------------------------- + +type __AgentName__Context = { + enabledSchemas: Set; + portRegistration?: { release: () => void }; + // Serializes concurrent updateAgentContext / closeAgentContext calls + // for this session so the (mutate set, await ensureSharedBridge, + // register port, bump refcount) sequence is atomic. Without this, + // an interleaved second enable could observe a non-empty set before + // the first call registers, skip registration itself, and then the + // first call rolling back on failure would leave the session + // "enabled" with no registration — and a later disable would + // decrement sharedRefCount it never incremented, tearing down the + // bridge another session still depends on. + pending?: Promise; +}; + +export function instantiate(): AppAgent { + return { + initializeAgentContext, + updateAgentContext, + closeAgentContext, + executeAction, + }; +} + +async function initializeAgentContext(): Promise<__AgentName__Context> { + return { enabledSchemas: new Set() }; +} + +// Chain `fn` after any in-flight operation for this session. Prior +// failures don't poison the chain (we swallow them when waiting), but +// the caller of `fn` still sees its own thrown error. +async function withSessionLock( + ctx: __AgentName__Context, + fn: () => Promise, +): Promise { + const prev = ctx.pending ?? Promise.resolve(); + let release!: () => void; + ctx.pending = new Promise((r) => (release = r)); + try { + await prev.catch(() => {}); + return await fn(); + } finally { + release(); + } +} + +/** + * Backstop cleanup invoked by the dispatcher when a session closes + * without an explicit per-schema disable (crash, client disconnect, + * shell shutdown). Releases this session's port registration and + * decrements the shared refcount once, even if multiple schemas were + * enabled. Idempotent — a subsequent `updateAgentContext(false, …)` + * will see an empty `enabledSchemas` and no-op. + */ +async function closeAgentContext( + context: SessionContext<__AgentName__Context>, +): Promise { + const ctx = context.agentContext; + await withSessionLock(ctx, async () => { + const hadRegistration = ctx.portRegistration !== undefined; + ctx.enabledSchemas.clear(); + ctx.portRegistration?.release(); + delete ctx.portRegistration; + if (!hadRegistration) return; + sharedRefCount = Math.max(0, sharedRefCount - 1); + if (sharedRefCount === 0 && sharedBridge) { + const bridge = sharedBridge; + sharedBridge = undefined; + sharedClosingPromise = bridge.close().finally(() => { + sharedClosingPromise = undefined; + }); + await sharedClosingPromise; + } + }); +} + +async function updateAgentContext( + enable: boolean, + context: SessionContext<__AgentName__Context>, + schemaName: string, +): Promise { + const ctx = context.agentContext; + await withSessionLock(ctx, async () => { + if (enable) { + if (ctx.enabledSchemas.has(schemaName)) return; + const bridge = await ensureSharedBridge(); + // Register + bump refcount only on the first schema for this + // session. `ctx.portRegistration` (not set size) is the source + // of truth for "this session has incremented sharedRefCount", + // so a later disable / closeAgentContext won't double-decrement + // even if a prior enable failed mid-way. + if (ctx.portRegistration === undefined) { + // Per-session registration: the registrar allows multiple + // entries for ("__agentName__", "default") across sessions and + // lookup returns the most recent, so each active session + // independently keeps the shared port discoverable. + ctx.portRegistration = context.registerPort( + "default", + bridge.port, + ); + sharedRefCount++; + } + ctx.enabledSchemas.add(schemaName); + } else { + if (!ctx.enabledSchemas.has(schemaName)) return; + ctx.enabledSchemas.delete(schemaName); + if ( + ctx.enabledSchemas.size === 0 && + ctx.portRegistration !== undefined + ) { + // Release this session's registration before potentially + // closing the server. Release is idempotent and a no-op if + // already released by the dispatcher's closeSessionContext + // backstop. + ctx.portRegistration.release(); + delete ctx.portRegistration; + sharedRefCount = Math.max(0, sharedRefCount - 1); + if (sharedRefCount === 0 && sharedBridge) { + const bridge = sharedBridge; + sharedBridge = undefined; + sharedClosingPromise = bridge.close().finally(() => { + sharedClosingPromise = undefined; + }); + await sharedClosingPromise; + } + } + } + }); +} + +async function executeAction( + action: TypeAgentAction<__AgentName__Actions>, + _context: ActionContext<__AgentName__Context>, +): Promise { + if (!sharedBridge?.connected) { + return { + error: "Host plugin not connected to the __agentName__ bridge. Start the plugin and ensure it is configured for the port reported by @system ports.", + }; + } + try { + const result = await sharedBridge.send( + action.actionName, + action.parameters, + ); + return createActionResultFromTextDisplay( + JSON.stringify(result, null, 2), + ); + } catch (err: any) { + return { error: err?.message ?? String(err) }; + } +} diff --git a/ts/packages/agents/onboarding/src/scaffolder/templates/websocketBridgeTemplate.ts b/ts/packages/agents/onboarding/src/scaffolder/templates/websocketBridgeTemplate.ts new file mode 100644 index 0000000000..7c34cc1931 --- /dev/null +++ b/ts/packages/agents/onboarding/src/scaffolder/templates/websocketBridgeTemplate.ts @@ -0,0 +1,202 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// WebSocket bridge for __agentName__. +// Manages a WebSocket connection to the host application plugin. +// Pattern matches the Excel/VS Code agent bridge implementations. +// +// Port allocation: the bridge binds on an OS-assigned ephemeral port +// (port=0) by default. Read the actual bound port from `.port` after +// `start()` resolves and register it with the dispatcher via +// `context.registerPort("default", bridge.port)` from your handler so +// external clients can discover it through the agent-server's +// discovery channel. Pass a fixed port to `start(port)` when debugging +// or when a host plugin expects a known address. + +import { WebSocketServer, WebSocket } from "ws"; +import { AddressInfo } from "net"; + +type BridgeCommand = { + id: string; + actionName: string; + parameters: Record; +}; + +type BridgeResponse = { + id: string; + success: boolean; + result?: unknown; + error?: string; +}; + +export class __AgentName__Bridge { + private clients = new Map(); + private nextClientId = 0; + private pending = new Map< + string, + { + resolve: (result: unknown) => void; + reject: (err: Error) => void; + } + >(); + + // Construction is private — use {@link __AgentName__Bridge.start} so + // callers always get a bridge that is guaranteed to be bound before + // they read {@link port} or pass it to the registrar. + private constructor( + private readonly server: WebSocketServer, + public readonly port: number, + ) { + this.server.on("connection", (ws) => { + const id = `c-${++this.nextClientId}`; + this.clients.set(id, ws); + ws.on("message", (data) => { + try { + const response = JSON.parse( + data.toString(), + ) as BridgeResponse; + const entry = this.pending.get(response.id); + if (entry) { + this.pending.delete(response.id); + if (response.success) entry.resolve(response.result); + else entry.reject(new Error(response.error)); + } + } catch { + // Ignore malformed payloads. + } + }); + // Reject any pending commands routed through the bridge when + // the last connected client drops; without this, in-flight + // callers hang until the bridge is closed. + const onDisconnect = () => { + this.clients.delete(id); + if (this.clients.size === 0 && this.pending.size > 0) { + const err = new Error( + "Host plugin disconnected before responding.", + ); + for (const entry of this.pending.values()) { + entry.reject(err); + } + this.pending.clear(); + } + }; + ws.on("close", onDisconnect); + ws.on("error", onDisconnect); + }); + } + + /** + * Bind a new bridge on `port`. Pass 0 (default) to let the OS pick + * a free ephemeral port; read the actual bound port from + * {@link port} after the returned promise resolves. Rejects on bind + * failure (EADDRINUSE under a fixed-port override) so callers see + * the problem instead of having it swallowed by a late error + * handler. + */ + public static start(port: number = 0): Promise<__AgentName__Bridge> { + return new Promise((resolve, reject) => { + const server = new WebSocketServer({ port }); + let settled = false; + const onError = (e: Error) => { + if (settled) return; + settled = true; + server.removeListener("listening", onListening); + reject(e); + }; + const onListening = () => { + if (settled) return; + settled = true; + server.removeListener("error", onError); + const addr = server.address() as AddressInfo | null; + if (!addr || typeof addr === "string") { + server.close(); + reject( + new Error( + "ws server.address() did not return AddressInfo", + ), + ); + return; + } + // Re-attach a permanent error handler so post-listen + // errors are surfaced rather than crashing the process. + server.on("error", (err) => { + console.error( + `[__agentName__Bridge] post-listen server error: ${err.message}`, + ); + }); + resolve(new __AgentName__Bridge(server, addr.port)); + }; + server.once("error", onError); + server.once("listening", onListening); + }); + } + + /** + * Close all client connections and the underlying server. Pending + * `sendCommand` promises are rejected so callers never hang on a + * closed bridge. Resolves when the server has fully released its + * port — important for a rapid restart cycle, where a synchronous + * return would race the new bind into EADDRINUSE. + */ + public close(): Promise { + const closedError = new Error( + "__AgentName__Bridge closed before response was received.", + ); + for (const entry of this.pending.values()) { + entry.reject(closedError); + } + this.pending.clear(); + for (const c of this.clients.values()) { + if (c.readyState === WebSocket.OPEN) c.close(); + } + this.clients.clear(); + return new Promise((resolve) => this.server.close(() => resolve())); + } + + public get connected(): boolean { + for (const c of this.clients.values()) { + if (c.readyState === WebSocket.OPEN) return true; + } + return false; + } + + public async sendCommand( + actionName: string, + parameters: Record, + ): Promise { + // Use the first OPEN client (single-plugin pattern). Adapt + // this selection if you need fan-out or per-session client + // targeting. + let target: WebSocket | undefined; + for (const c of this.clients.values()) { + if (c.readyState === WebSocket.OPEN) { + target = c; + break; + } + } + if (!target) { + throw new Error("No client connected to the __agentName__ bridge."); + } + const id = `cmd-${Date.now()}-${Math.random().toString(36).slice(2)}`; + return new Promise((resolve, reject) => { + this.pending.set(id, { resolve, reject }); + target!.send( + JSON.stringify({ + id, + actionName, + parameters, + } satisfies BridgeCommand), + (err) => { + // ws.send errors surface here; without this, a send + // failure (socket closed between readyState check and + // send) would leak the pending entry and hang the + // caller. + if (err) { + this.pending.delete(id); + reject(err); + } + }, + ); + }); + } +} diff --git a/ts/packages/agents/onboarding/src/tsconfig.json b/ts/packages/agents/onboarding/src/tsconfig.json index 85efcd566d..add5e087d7 100644 --- a/ts/packages/agents/onboarding/src/tsconfig.json +++ b/ts/packages/agents/onboarding/src/tsconfig.json @@ -6,6 +6,8 @@ "outDir": "../dist" }, "include": ["./**/*"], + "exclude": ["./scaffolder/templates/**/*"], + "references": [{ "path": "./scaffolder/templates" }], "ts-node": { "esm": true } diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml index b3ca5597c4..1947616a26 100644 --- a/ts/pnpm-lock.yaml +++ b/ts/pnpm-lock.yaml @@ -155,7 +155,7 @@ importers: version: 8.18.1 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@25.9.1)(ts-node@10.9.2(@types/node@25.9.1)(typescript@5.4.5)) + version: 29.7.0(@types/node@22.19.19)(ts-node@10.9.2(@types/node@22.19.19)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -906,7 +906,7 @@ importers: version: 12.0.2(webpack@5.105.0) jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.19.19)(ts-node@10.9.2(@types/node@22.19.19)(typescript@5.4.5)) + version: 29.7.0(@types/node@25.9.1)(ts-node@10.9.2(@types/node@25.9.1)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -2734,6 +2734,12 @@ importers: '@types/debug': specifier: ^4.1.12 version: 4.1.12 + '@types/node': + specifier: ^22.0.0 + version: 22.19.19 + '@types/ws': + specifier: ^8.5.10 + version: 8.18.1 action-grammar-compiler: specifier: workspace:* version: link:../../actionGrammarCompiler @@ -2752,6 +2758,9 @@ importers: typescript: specifier: ~5.4.5 version: 5.4.5 + ws: + specifier: ^8.18.0 + version: 8.21.0 packages/agents/osNotifications: dependencies: @@ -16641,7 +16650,7 @@ snapshots: '@anthropic-ai/claude-agent-sdk@0.2.105': dependencies: - '@anthropic-ai/sdk': 0.81.0(zod@4.1.13) + '@anthropic-ai/sdk': 0.81.0(zod@3.25.76) '@modelcontextprotocol/sdk': 1.29.0 optionalDependencies: '@img/sharp-darwin-arm64': 0.34.5 @@ -19985,7 +19994,7 @@ snapshots: json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 raw-body: 3.0.2 - zod-to-json-schema: 3.25.1(zod@4.1.13) + zod-to-json-schema: 3.25.1(zod@3.25.76) transitivePeerDependencies: - supports-color