From 38685afe19041e88f480a13d2c37087c3a0f25c2 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Fri, 3 Apr 2026 19:22:50 -0700 Subject: [PATCH 01/33] added onboarding agent --- ts/packages/agents/onboarding/AGENTS.md | 76 +++ ts/packages/agents/onboarding/README.md | 84 +++ ts/packages/agents/onboarding/USER_GUIDE.md | 523 +++++++++++++++++ ts/packages/agents/onboarding/package.json | 62 ++ .../src/discovery/discoveryHandler.ts | 369 ++++++++++++ .../src/discovery/discoverySchema.agr | 71 +++ .../src/discovery/discoverySchema.ts | 54 ++ .../src/grammarGen/grammarGenHandler.ts | 191 ++++++ .../src/grammarGen/grammarGenSchema.agr | 49 ++ .../src/grammarGen/grammarGenSchema.ts | 34 ++ ts/packages/agents/onboarding/src/lib/llm.ts | 35 ++ .../agents/onboarding/src/lib/workspace.ts | 214 +++++++ .../onboarding/src/onboardingActionHandler.ts | 285 +++++++++ .../onboarding/src/onboardingManifest.json | 77 +++ .../onboarding/src/onboardingSchema.agr | 79 +++ .../agents/onboarding/src/onboardingSchema.ts | 57 ++ .../src/packaging/packagingHandler.ts | 252 ++++++++ .../src/packaging/packagingSchema.agr | 35 ++ .../src/packaging/packagingSchema.ts | 27 + .../src/phraseGen/phraseGenHandler.ts | 277 +++++++++ .../src/phraseGen/phraseGenSchema.agr | 66 +++ .../src/phraseGen/phraseGenSchema.ts | 56 ++ .../src/scaffolder/scaffolderHandler.ts | 516 +++++++++++++++++ .../src/scaffolder/scaffolderSchema.agr | 47 ++ .../src/scaffolder/scaffolderSchema.ts | 38 ++ .../src/schemaGen/schemaGenHandler.ts | 193 +++++++ .../src/schemaGen/schemaGenSchema.agr | 54 ++ .../src/schemaGen/schemaGenSchema.ts | 38 ++ .../onboarding/src/testing/testingHandler.ts | 545 ++++++++++++++++++ .../onboarding/src/testing/testingSchema.agr | 77 +++ .../onboarding/src/testing/testingSchema.ts | 60 ++ .../agents/onboarding/src/tsconfig.json | 12 + ts/packages/agents/onboarding/tsconfig.json | 11 + 33 files changed, 4564 insertions(+) create mode 100644 ts/packages/agents/onboarding/AGENTS.md create mode 100644 ts/packages/agents/onboarding/README.md create mode 100644 ts/packages/agents/onboarding/USER_GUIDE.md create mode 100644 ts/packages/agents/onboarding/package.json create mode 100644 ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts create mode 100644 ts/packages/agents/onboarding/src/discovery/discoverySchema.agr create mode 100644 ts/packages/agents/onboarding/src/discovery/discoverySchema.ts create mode 100644 ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts create mode 100644 ts/packages/agents/onboarding/src/grammarGen/grammarGenSchema.agr create mode 100644 ts/packages/agents/onboarding/src/grammarGen/grammarGenSchema.ts create mode 100644 ts/packages/agents/onboarding/src/lib/llm.ts create mode 100644 ts/packages/agents/onboarding/src/lib/workspace.ts create mode 100644 ts/packages/agents/onboarding/src/onboardingActionHandler.ts create mode 100644 ts/packages/agents/onboarding/src/onboardingManifest.json create mode 100644 ts/packages/agents/onboarding/src/onboardingSchema.agr create mode 100644 ts/packages/agents/onboarding/src/onboardingSchema.ts create mode 100644 ts/packages/agents/onboarding/src/packaging/packagingHandler.ts create mode 100644 ts/packages/agents/onboarding/src/packaging/packagingSchema.agr create mode 100644 ts/packages/agents/onboarding/src/packaging/packagingSchema.ts create mode 100644 ts/packages/agents/onboarding/src/phraseGen/phraseGenHandler.ts create mode 100644 ts/packages/agents/onboarding/src/phraseGen/phraseGenSchema.agr create mode 100644 ts/packages/agents/onboarding/src/phraseGen/phraseGenSchema.ts create mode 100644 ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts create mode 100644 ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.agr create mode 100644 ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.ts create mode 100644 ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts create mode 100644 ts/packages/agents/onboarding/src/schemaGen/schemaGenSchema.agr create mode 100644 ts/packages/agents/onboarding/src/schemaGen/schemaGenSchema.ts create mode 100644 ts/packages/agents/onboarding/src/testing/testingHandler.ts create mode 100644 ts/packages/agents/onboarding/src/testing/testingSchema.agr create mode 100644 ts/packages/agents/onboarding/src/testing/testingSchema.ts create mode 100644 ts/packages/agents/onboarding/src/tsconfig.json create mode 100644 ts/packages/agents/onboarding/tsconfig.json diff --git a/ts/packages/agents/onboarding/AGENTS.md b/ts/packages/agents/onboarding/AGENTS.md new file mode 100644 index 0000000000..b94265d4f6 --- /dev/null +++ b/ts/packages/agents/onboarding/AGENTS.md @@ -0,0 +1,76 @@ +# AGENTS.md — Onboarding Agent + +This document is for AI agents (Claude Code, GitHub Copilot, etc.) working with the onboarding agent codebase. + +## What this agent does + +The onboarding agent automates integrating a new application or API into TypeAgent. It is itself a TypeAgent agent, so its actions are available to AI orchestrators via TypeAgent's MCP interface using `list_commands`. + +## Agent structure + +``` +src/ + onboardingManifest.json ← main manifest, declares 7 sub-action manifests + onboardingSchema.ts ← top-level coordination actions + onboardingSchema.agr ← grammar for top-level actions + onboardingActionHandler.ts ← instantiate(); routes all actions to phase handlers + lib/ + workspace.ts ← read/write per-integration state on disk + llm.ts ← aiclient ChatModel factories per phase + discovery/ ← Phase 1: API surface enumeration + phraseGen/ ← Phase 2: natural language phrase generation + schemaGen/ ← Phase 3: TypeScript action schema generation + grammarGen/ ← Phase 4: .agr grammar generation + scaffolder/ ← Phase 5: agent package scaffolding + testing/ ← Phase 6: phrase→action test loop + packaging/ ← Phase 7: packaging and distribution +``` + +## How actions are routed + +`onboardingActionHandler.ts` exports `instantiate()` which returns a single `AppAgent`. The `executeAction` method receives all actions (from main schema and all sub-schemas) and dispatches by `action.actionName` to the appropriate phase handler module. + +## Workspace state + +All artifacts are persisted at `~/.typeagent/onboarding//`. The `workspace.ts` lib provides: + +- `createWorkspace(config)` — initialize a new integration workspace +- `loadState(name)` — load current phase state +- `saveState(state)` — persist state +- `updatePhase(name, phase, update)` — update phase status; automatically advances `currentPhase` on approval +- `readArtifact(name, phase, filename)` — read a phase artifact +- `writeArtifact(name, phase, filename, content)` — write a phase artifact +- `listIntegrations()` — list all integration workspaces + +## LLM usage + +Each phase that requires LLM calls uses `aiclient`'s `createChatModelDefault(tag)`. Tags are namespaced as `onboarding:` (e.g. `onboarding:schemagen`). This follows the standard TypeAgent pattern — credentials come from `ts/.env`. + +## Phase approval model + +Each phase has a status: `pending → in-progress → approved`. An `approve*` action locks artifacts and advances to the next phase. The AI orchestrator is expected to review artifacts before calling approve — this is the human-in-the-loop checkpoint. + +## Adding a new phase + +1. Create `src//` with `*Schema.ts`, `*Schema.agr`, `*Handler.ts` +2. Add the sub-action manifest entry to `onboardingManifest.json` +3. Add `asc:*` and `agc:*` build scripts to `package.json` +4. Import and wire up the handler in `onboardingActionHandler.ts` +5. Add the phase to the `OnboardingPhase` type and `phases` object in `workspace.ts` + +## Adding a new tool to an existing phase + +1. Add the action type to the phase's `*Schema.ts` +2. Add grammar patterns to the phase's `*Schema.agr` +3. Implement the handler case in the phase's `*Handler.ts` + +## Key dependencies + +- `@typeagent/agent-sdk` — `AppAgent`, `ActionContext`, `TypeAgentAction`, `ActionResult` +- `@typeagent/agent-sdk/helpers/action` — `createActionResultFromTextDisplay`, `createActionResultFromMarkdownDisplay` +- `aiclient` — `createChatModelDefault`, `ChatModel` +- `typechat` — `createJsonTranslator` for structured LLM output + +## Testing + +Run phrase→action tests with the `runTests` action after completing the testing phase setup. Results are saved to `~/.typeagent/onboarding//testing/results.json`. The `proposeRepair` action uses an LLM to suggest schema/grammar fixes for failing tests. diff --git a/ts/packages/agents/onboarding/README.md b/ts/packages/agents/onboarding/README.md new file mode 100644 index 0000000000..97b280cc43 --- /dev/null +++ b/ts/packages/agents/onboarding/README.md @@ -0,0 +1,84 @@ +# Onboarding Agent + +A TypeAgent agent that automates the end-to-end process of integrating a new application or API into TypeAgent. Each phase of the onboarding pipeline is a sub-agent with typed actions, enabling AI orchestrators (Claude Code, GitHub Copilot) to drive the process via TypeAgent's MCP interface. + +## Overview + +Integrating a new application into TypeAgent involves 7 phases: + +| Phase | Sub-agent | What it does | +|---|---|---| +| 1 | `onboarding-discovery` | Crawls docs or parses an OpenAPI spec to enumerate the API surface | +| 2 | `onboarding-phrasegen` | Generates natural language sample phrases for each action | +| 3 | `onboarding-schemagen` | Generates TypeScript action schemas from the API surface | +| 4 | `onboarding-grammargen` | Generates `.agr` grammar files from schemas and phrases | +| 5 | `onboarding-scaffolder` | Stamps out the agent package infrastructure | +| 6 | `onboarding-testing` | Generates test cases and runs a phrase→action validation loop | +| 7 | `onboarding-packaging` | Packages the agent for distribution and registration | + +Each phase produces **artifacts saved to disk** at `~/.typeagent/onboarding//`, so work can be resumed across sessions. + +## Usage + +### Starting a new integration + +``` +start onboarding for slack +``` + +### Checking status + +``` +what's the status of the slack onboarding +``` + +### Resuming an in-progress integration + +``` +resume onboarding for slack +``` + +### Running a specific phase + +``` +crawl docs at https://api.slack.com/docs for slack +generate phrases for slack +generate schema for slack +run tests for slack +``` + +## Workspace layout + +``` +~/.typeagent/onboarding/ + / + state.json ← phase status, config, timestamps + discovery/ + api-surface.json ← enumerated actions from docs/spec + phraseGen/ + phrases.json ← sample phrases per action + schemaGen/ + schema.ts ← generated TypeScript action schema + grammarGen/ + schema.agr ← generated grammar file + scaffolder/ + agent/ ← stamped-out agent package files + testing/ + test-cases.json ← phrase → expected action test pairs + results.json ← latest test run results + packaging/ + dist/ ← final packaged output +``` + +Each phase must be **approved** before the next phase begins. Approval locks the phase's artifacts and advances the current phase pointer in `state.json`. + +## Building + +```bash +pnpm install +pnpm run build +``` + +## Architecture + +See [AGENTS.md](./AGENTS.md) for details on the agent structure, how to extend it, and how each phase's LLM prompting works. diff --git a/ts/packages/agents/onboarding/USER_GUIDE.md b/ts/packages/agents/onboarding/USER_GUIDE.md new file mode 100644 index 0000000000..441e134b87 --- /dev/null +++ b/ts/packages/agents/onboarding/USER_GUIDE.md @@ -0,0 +1,523 @@ +# TypeAgent Onboarding — User Guide + +This guide shows how to use an AI assistant (Claude Code, GitHub Copilot, or any MCP client) to onboard a new application or API into TypeAgent from start to finish. + +The onboarding agent is itself a TypeAgent agent. Its actions are available in your AI assistant automatically via `discover_agents` — no extra registration required beyond the one-time MCP setup below. + +--- + +## Step 0 — Register TypeAgent as an MCP server + +Before you can use the onboarding agent from your AI assistant, you need to register TypeAgent's MCP server (`command-executor`) once. This is a one-time setup per machine. + +### What it is + +TypeAgent exposes a stdio MCP server at `ts/packages/commandExecutor/dist/server.js`. It provides three tools to your AI assistant: + +| Tool | What it does | +|---|---| +| `discover_agents` | Lists all TypeAgent agents and their actions | +| `execute_action` | Calls any agent action directly by name with typed parameters | +| `execute_command` | Passes a natural language request to the TypeAgent dispatcher | + +The onboarding agent's actions (`startOnboarding`, `crawlDocUrl`, `generateSchema`, etc.) are discovered and called via these tools. + +### Prerequisites + +- Node.js ≥ 20 installed +- The TypeAgent repo cloned and built: `cd ts && pnpm install && pnpm run build` +- The TypeAgent agent-server running (started automatically on first use, or via `node packages/agentServer/server/dist/server.js` from `ts/`) +- `ts/.env` configured with your Azure OpenAI or OpenAI API keys + +--- + +### Claude Code + +Claude Code reads MCP server config from `.mcp.json` in your project root (or `~/.claude/mcp.json` for global config). + +The repo already includes `ts/.mcp.json` with the `command-executor` server. **If you open Claude Code from the `ts/` directory, it will be picked up automatically.** + +To verify it is active, run inside Claude Code: + +``` +/mcp +``` + +You should see `command-executor` listed as connected. + +**If you need to register it manually** (e.g. you're working from a different directory), add this to your `.mcp.json`: + +```json +{ + "mcpServers": { + "typeagent": { + "command": "node", + "args": ["/ts/packages/commandExecutor/dist/server.js"], + "env": {} + } + } +} +``` + +Replace `` with the full path to your TypeAgent clone, for example: +- Windows: `C:/repos/TypeAgent/ts/packages/commandExecutor/dist/server.js` +- Mac/Linux: `/home/you/repos/TypeAgent/ts/packages/commandExecutor/dist/server.js` + +Then restart Claude Code. + +--- + +### GitHub Copilot (VS Code) + +GitHub Copilot uses VS Code's MCP configuration. Add the TypeAgent server via the VS Code settings UI or directly in `settings.json`. + +**Via settings.json** — open your VS Code `settings.json` (`Ctrl+Shift+P` → "Open User Settings (JSON)") and add: + +```json +{ + "github.copilot.chat.mcpServers": { + "typeagent": { + "command": "node", + "args": ["/ts/packages/commandExecutor/dist/server.js"], + "type": "stdio" + } + } +} +``` + +**Via the VS Code UI** — open the Command Palette (`Ctrl+Shift+P`), run **"MCP: Add MCP Server"**, choose **"Command (stdio)"**, and enter: +- Command: `node` +- Args: `/ts/packages/commandExecutor/dist/server.js` +- Name: `typeagent` + +After saving, open a Copilot Chat panel. You should see the TypeAgent tools listed under the MCP tools icon (the plug icon in the chat input bar). + +--- + +### Verify the connection + +Once registered, ask your AI assistant: + +``` +> Discover TypeAgent agents +``` + +or + +``` +> What TypeAgent agents are available? +``` + +The assistant will call `discover_agents` and return a list that includes `onboarding` (among others). If you see the list, you're ready to start onboarding. + +**Troubleshooting:** +- If the server isn't found, check that `ts/packages/commandExecutor/dist/server.js` exists — run `pnpm run build` from `ts/` if not +- If tools don't appear, restart your AI assistant or reload the VS Code window +- Logs are written to `~/.tmp/typeagent-mcp/` — check there for connection errors + +--- + +## Prerequisites (after MCP setup) + +- TypeAgent MCP server registered with your AI assistant (see above) +- Your `ts/.env` configured with API keys (the same ones TypeAgent already uses) +- The application you want to integrate is either documented online or has an OpenAPI spec + +--- + +## How it works + +You talk to your AI assistant in plain English. The assistant calls the onboarding agent's actions to do the work. Each phase produces artifacts saved to `~/.typeagent/onboarding//` so you can pause and come back anytime. + +``` +You (natural language) + ↓ +AI assistant (Claude Code / Copilot) + ↓ MCP → list_commands → TypeAgent +Onboarding agent actions + ↓ +Artifacts on disk (schemas, phrases, grammar, agent package) +``` + +--- + +## Complete walkthrough: onboarding a REST API + +Below is a realistic session. Lines starting with `>` are things you'd say to your AI assistant. + +--- + +### Step 1 — Start the onboarding + +``` +> Start onboarding for Slack +``` + +The assistant calls `startOnboarding` and creates a workspace at `~/.typeagent/onboarding/slack/`. + +--- + +### Step 2 — Discover the API surface + +**From documentation URL:** + +``` +> Crawl the Slack API docs at https://api.slack.com/methods for slack +``` + +**From an OpenAPI spec file:** + +``` +> Parse the OpenAPI spec at C:\specs\slack-openapi.json for slack +``` + +**From an OpenAPI spec URL:** + +``` +> Parse the OpenAPI spec at https://raw.githubusercontent.com/slackapi/slack-api-specs/master/web-api/slack_web_openapi_v2.json for slack +``` + +After crawling, review what was found: + +``` +> List the discovered actions for slack +``` + +You'll see a table of all API actions with names and descriptions. Trim down to what you actually want: + +``` +> Approve the API surface for slack, excluding: listAllUsers, adminCreateWorkspace, deleteTeam +``` + +Or include only specific actions: + +``` +> Approve the API surface for slack, including only: postMessage, listChannels, getUserInfo, addReaction, uploadFile +``` + +--- + +### Step 3 — Generate sample phrases + +``` +> Generate phrases for slack +``` + +The assistant calls `generatePhrases` and asks the LLM to produce 5 natural language samples per action. You can tune the count: + +``` +> Generate 8 phrases per action for slack +``` + +Review the output. Add or remove specific phrases: + +``` +> Add phrase "DM John about the meeting" for action postMessage in slack +> Remove phrase "send a slack" from action postMessage in slack +``` + +When satisfied: + +``` +> Approve phrases for slack +``` + +--- + +### Step 4 — Generate the TypeScript action schema + +``` +> Generate the action schema for slack +``` + +The LLM produces a TypeScript file with union types and JSDoc comments mapping each action to the Slack API. Review the output in the response. + +If you want changes: + +``` +> Refine the slack schema to make the channelId parameter optional and add a threadTs parameter to postMessage +``` + +``` +> Refine the slack schema to split postMessage into postChannelMessage and postDirectMessage +``` + +When happy: + +``` +> Approve the slack schema +``` + +--- + +### Step 5 — Generate the grammar + +``` +> Generate the grammar for slack +``` + +The LLM produces a `.agr` file with natural language patterns for each action. Then validate it compiles: + +``` +> Compile the slack grammar +``` + +If compilation fails, the error message will tell you which rule is invalid. You can ask: + +``` +> Generate the grammar for slack +``` +again after the schema is adjusted, or manually edit the grammar file at `~/.typeagent/onboarding/slack/grammarGen/schema.agr`. + +When the grammar compiles cleanly: + +``` +> Approve the slack grammar +``` + +--- + +### Step 6 — Scaffold the agent package + +``` +> Scaffold the slack agent +``` + +This stamps out a complete TypeAgent agent package at `ts/packages/agents/slack/` with: +- `slackManifest.json` +- `slackSchema.ts` (the approved schema) +- `slackSchema.agr` (the approved grammar) +- `slackActionHandler.ts` (stub — ready for your implementation) +- `package.json`, `tsconfig.json`, `src/tsconfig.json` + +If your integration talks to Slack over REST, scaffold the bridge too: + +``` +> Scaffold the slack rest-client plugin +``` + +For a WebSocket-based integration (like Excel or VS Code agents): + +``` +> Scaffold the slack websocket-bridge plugin +``` + +For an Office add-in: + +``` +> Scaffold the slack office-addin plugin +``` + +See what templates are available: + +``` +> List templates +``` + +--- + +### Step 7 — Package and register + +``` +> Package the slack agent +``` + +This runs `pnpm install` and `pnpm run build` in the agent directory. + +To also register it with the local TypeAgent dispatcher immediately: + +``` +> Package the slack agent and register it +``` + +Then restart TypeAgent so it picks up the new agent. + +--- + +### Step 8 — Run the tests + +After TypeAgent has restarted with the agent registered: + +``` +> Generate tests for slack +> Run tests for slack +``` + +You'll get a pass/fail table. If tests fail: + +``` +> Get the failing test results for slack +> Propose a repair for slack +``` + +The LLM analyzes the failures and suggests specific changes to the schema and/or grammar. Review the proposal, then: + +``` +> Approve the repair for slack +``` + +Then re-run: + +``` +> Run tests for slack +``` + +Repeat until pass rate is satisfactory. A common target is >90% before handing off to users. + +--- + +## Checking in on progress + +At any point: + +``` +> What's the status of the slack onboarding? +``` + +You'll see a phase-by-phase table like: + +``` +| Phase | Status | +|-------------|------------| +| discovery | ✅ approved | +| phraseGen | ✅ approved | +| schemaGen | ✅ approved | +| grammarGen | 🔄 in-progress | +| scaffolder | ⏳ pending | +| testing | ⏳ pending | +| packaging | ⏳ pending | +``` + +--- + +## Resuming an interrupted onboarding + +If you close your session and come back later: + +``` +> Resume onboarding for slack +``` + +This tells you the current phase and what to do next. All artifacts are on disk — nothing is lost. + +List all integrations in progress: + +``` +> List my integrations +> List integrations in progress +``` + +--- + +## Implementing the action handler + +After scaffolding, you'll have a stub handler at `ts/packages/agents/slack/src/slackActionHandler.ts`. The stub looks like: + +```typescript +async function executeAction( + action: TypeAgentAction, + context: ActionContext, +): Promise { + // TODO: implement action handlers + return createActionResultFromTextDisplay( + `Executing ${action.actionName} — not yet implemented.`, + ); +} +``` + +Fill in the cases using the Slack Web API client. For example: + +```typescript +switch (action.actionName) { + case "postMessage": { + const result = await slackClient.chat.postMessage({ + channel: action.parameters.channelId, + text: action.parameters.message, + }); + return createActionResultFromTextDisplay( + `Message sent to ${action.parameters.channelId}`, + ); + } + // ... +} +``` + +After implementing, rebuild: + +```bash +cd ts/packages/agents/slack +pnpm run build +``` + +Then re-run tests: + +``` +> Run tests for slack +``` + +--- + +## Sample session: rapid onboarding via AI assistant + +Here's a compressed example of what a full session looks like from the AI assistant's perspective: + +``` +You: I want to add a Jira integration to TypeAgent. It should support creating + issues, searching issues, updating issue status, and adding comments. + +AI: I'll kick off the Jira onboarding. Let me start by discovering the API surface. + + [calls startOnboarding for jira] + [calls parseOpenApiSpec from https://developer.atlassian.com/cloud/jira/platform/swagger-v3.v3.json] + Found 147 API actions. Here are a few: + - createIssue: Creates a new issue + - searchIssues: Searches for issues using JQL + - transitionIssue: Transitions an issue to a new status + - addComment: Adds a comment to an issue + ... + + I'll approve just the 4 you specified. + [calls approveApiSurface with includeActions: [createIssue, searchIssues, transitionIssue, addComment]] + [calls generatePhrases] + [calls approvePhrases] + [calls generateSchema] + + Here's the generated schema — looks good, I'll approve it. + [calls approveSchema] + [calls generateGrammar] + [calls compileGrammar] ✓ Compiled successfully + [calls approveGrammar] + [calls scaffoldAgent] + + The Jira agent is scaffolded at ts/packages/agents/jira/. Now I'll package + and register it so we can run tests. + + [calls packageAgent with register: true] + + TypeAgent needs a restart to pick up the new agent. Once you've restarted: + + [calls generateTests] + [calls runTests] + + Pass rate: 87% (35/40). Proposing repair for the 5 failing tests... + [calls proposeRepair] + [calls approveRepair] + [calls runTests] + + Pass rate: 97% (39/40). One edge case remaining in searchIssues — acceptable. + Onboarding complete! Implement slackActionHandler.ts to wire up the Jira API calls. +``` + +--- + +## Tips + +**Start narrow.** Onboard 4–6 actions first, get them working end-to-end, then add more. You can re-run `generatePhrases`, `generateSchema`, and `generateGrammar` incrementally. + +**Inspect artifacts directly.** All generated files are in `~/.typeagent/onboarding//`. You can edit them by hand before approving if the LLM output isn't quite right. + +**Grammar failures are normal.** The `.agr` compiler is strict. If `compileGrammar` fails, ask the AI to regenerate the grammar, or read the error and fix the specific rule. Common issues are ambiguous wildcards and missing required words before captures. + +**Test failures drive improvement.** A 70% pass rate on first run is typical. Two rounds of `proposeRepair` → `runTests` usually gets to 90%+. The LLM is good at diagnosing pattern mismatches. + +**Re-use grows over time.** The second integration you onboard will reuse the doc crawler, phrase generator, and schema generator — only the integration-specific configuration changes. diff --git a/ts/packages/agents/onboarding/package.json b/ts/packages/agents/onboarding/package.json new file mode 100644 index 0000000000..81d4d861a3 --- /dev/null +++ b/ts/packages/agents/onboarding/package.json @@ -0,0 +1,62 @@ +{ + "name": "onboarding-agent", + "version": "0.0.1", + "private": true, + "description": "TypeAgent onboarding agent — automates integrating new applications into TypeAgent", + "homepage": "https://github.com/microsoft/TypeAgent#readme", + "repository": { + "type": "git", + "url": "https://github.com/microsoft/TypeAgent.git", + "directory": "ts/packages/agents/onboarding" + }, + "license": "MIT", + "author": "Microsoft", + "type": "module", + "exports": { + "./agent/manifest": "./src/onboardingManifest.json", + "./agent/handlers": "./dist/onboardingActionHandler.js" + }, + "scripts": { + "asc:main": "asc -i ./src/onboardingSchema.ts -o ./dist/onboardingSchema.pas.json -t OnboardingActions", + "agc:main": "agc -i ./src/onboardingSchema.agr -o ./dist/onboardingSchema.ag.json", + "asc:discovery": "asc -i ./src/discovery/discoverySchema.ts -o ./dist/discoverySchema.pas.json -t DiscoveryActions", + "agc:discovery": "agc -i ./src/discovery/discoverySchema.agr -o ./dist/discoverySchema.ag.json", + "asc:phrasegen": "asc -i ./src/phraseGen/phraseGenSchema.ts -o ./dist/phraseGenSchema.pas.json -t PhraseGenActions", + "agc:phrasegen": "agc -i ./src/phraseGen/phraseGenSchema.agr -o ./dist/phraseGenSchema.ag.json", + "asc:schemagen": "asc -i ./src/schemaGen/schemaGenSchema.ts -o ./dist/schemaGenSchema.pas.json -t SchemaGenActions", + "agc:schemagen": "agc -i ./src/schemaGen/schemaGenSchema.agr -o ./dist/schemaGenSchema.ag.json", + "asc:grammargen": "asc -i ./src/grammarGen/grammarGenSchema.ts -o ./dist/grammarGenSchema.pas.json -t GrammarGenActions", + "agc:grammargen": "agc -i ./src/grammarGen/grammarGenSchema.agr -o ./dist/grammarGenSchema.ag.json", + "asc:scaffolder": "asc -i ./src/scaffolder/scaffolderSchema.ts -o ./dist/scaffolderSchema.pas.json -t ScaffolderActions", + "agc:scaffolder": "agc -i ./src/scaffolder/scaffolderSchema.agr -o ./dist/scaffolderSchema.ag.json", + "asc:testing": "asc -i ./src/testing/testingSchema.ts -o ./dist/testingSchema.pas.json -t TestingActions", + "agc:testing": "agc -i ./src/testing/testingSchema.agr -o ./dist/testingSchema.ag.json", + "asc:packaging": "asc -i ./src/packaging/packagingSchema.ts -o ./dist/packagingSchema.pas.json -t PackagingActions", + "agc:packaging": "agc -i ./src/packaging/packagingSchema.agr -o ./dist/packagingSchema.ag.json", + "build": "concurrently npm:tsc npm:asc:* npm:agc:*", + "clean": "rimraf --glob dist *.tsbuildinfo *.done.build.log", + "prettier": "prettier --check . --ignore-path ../../../.prettierignore", + "prettier:fix": "prettier --write . --ignore-path ../../../.prettierignore", + "tsc": "tsc -b" + }, + "dependencies": { + "@typeagent/agent-sdk": "workspace:*", + "@typeagent/dispatcher-types": "workspace:*", + "agent-dispatcher": "workspace:*", + "aiclient": "workspace:*", + "default-agent-provider": "workspace:*", + "dispatcher-node-providers": "workspace:*", + "typechat": "^0.1.1" + }, + "devDependencies": { + "@typeagent/action-schema-compiler": "workspace:*", + "action-grammar-compiler": "workspace:*", + "concurrently": "^9.1.2", + "prettier": "^3.5.3", + "rimraf": "^6.0.1", + "typescript": "~5.4.5" + }, + "engines": { + "node": ">=20" + } +} diff --git a/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts b/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts new file mode 100644 index 0000000000..1da0d27226 --- /dev/null +++ b/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts @@ -0,0 +1,369 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Phase 1 — Discovery handler. +// Enumerates the API surface of the target application from documentation +// or an OpenAPI spec, saving results to the workspace for the next phase. + +import { + ActionContext, + TypeAgentAction, + ActionResult, +} from "@typeagent/agent-sdk"; +import { + createActionResultFromTextDisplay, + createActionResultFromMarkdownDisplay, +} from "@typeagent/agent-sdk/helpers/action"; +import { DiscoveryActions } from "./discoverySchema.js"; +import { + loadState, + updatePhase, + writeArtifactJson, + readArtifactJson, +} from "../lib/workspace.js"; +import { getDiscoveryModel } from "../lib/llm.js"; + +// Represents a single discovered API action +export type DiscoveredAction = { + name: string; + description: string; + // HTTP method if REST, or operation type + method?: string; + // Endpoint path or function signature + path?: string; + // Discovered parameters + parameters?: DiscoveredParameter[]; + // Source URL where this was found + sourceUrl?: string; +}; + +export type DiscoveredParameter = { + name: string; + type: string; + description?: string; + required?: boolean; +}; + +export type ApiSurface = { + integrationName: string; + discoveredAt: string; + source: string; + actions: DiscoveredAction[]; + approved?: boolean; + approvedAt?: string; + approvedActions?: string[]; +}; + +export async function executeDiscoveryAction( + action: TypeAgentAction, + _context: ActionContext, +): Promise { + switch (action.actionName) { + case "crawlDocUrl": + return handleCrawlDocUrl( + action.parameters.integrationName, + action.parameters.url, + action.parameters.maxDepth ?? 2, + ); + + case "parseOpenApiSpec": + return handleParseOpenApiSpec( + action.parameters.integrationName, + action.parameters.specSource, + ); + + case "listDiscoveredActions": + return handleListDiscoveredActions( + action.parameters.integrationName, + ); + + case "approveApiSurface": + return handleApproveApiSurface( + action.parameters.integrationName, + action.parameters.includeActions, + action.parameters.excludeActions, + ); + } +} + +async function handleCrawlDocUrl( + integrationName: string, + url: string, + maxDepth: number, +): Promise { + const state = await loadState(integrationName); + if (!state) { + return { error: `Integration "${integrationName}" not found. Run startOnboarding first.` }; + } + + await updatePhase(integrationName, "discovery", { status: "in-progress" }); + + const model = getDiscoveryModel(); + + // Fetch and parse the documentation page + let pageContent: string; + try { + const response = await fetch(url); + if (!response.ok) { + return { error: `Failed to fetch ${url}: ${response.status} ${response.statusText}` }; + } + pageContent = await response.text(); + } catch (err: any) { + return { error: `Failed to fetch ${url}: ${err?.message ?? err}` }; + } + + // Use LLM to extract API actions from the page content + const prompt = [ + { + role: "system" as const, + content: + "You are an API documentation analyzer. Extract a list of API actions/operations from the provided documentation HTML. " + + "For each action, identify: name (camelCase), description, HTTP method (if applicable), endpoint path (if applicable), and parameters. " + + "Return a JSON array of actions.", + }, + { + role: "user" as const, + content: + `Extract all API actions from this documentation page for the "${integrationName}" integration.\n\n` + + `URL: ${url}\n\n` + + `Content (truncated to 8000 chars):\n${pageContent.slice(0, 8000)}`, + }, + ]; + + const result = await model.complete(prompt); + if (!result.success) { + return { error: `LLM extraction failed: ${result.message}` }; + } + + let actions: DiscoveredAction[] = []; + try { + // Extract JSON from LLM response + const jsonMatch = result.data.match(/\[[\s\S]*\]/); + if (jsonMatch) { + actions = JSON.parse(jsonMatch[0]); + } + } catch { + return { error: "Failed to parse LLM response as JSON action list." }; + } + + // Add source URL to each action + actions = actions.map((a) => ({ ...a, sourceUrl: url })); + + // Merge with any existing discovered actions + const existing = await readArtifactJson( + integrationName, + "discovery", + "api-surface.json", + ); + const merged: ApiSurface = { + integrationName, + discoveredAt: new Date().toISOString(), + source: url, + actions: [ + ...(existing?.actions ?? []).filter((a) => + !actions.find((n) => n.name === a.name), + ), + ...actions, + ], + }; + + await writeArtifactJson( + integrationName, + "discovery", + "api-surface.json", + merged, + ); + + return createActionResultFromMarkdownDisplay( + `## Discovery complete: ${integrationName}\n\n` + + `**Source:** ${url}\n` + + `**Actions found:** ${actions.length}\n\n` + + actions + .slice(0, 20) + .map((a) => `- **${a.name}**: ${a.description}`) + .join("\n") + + (actions.length > 20 + ? `\n\n_...and ${actions.length - 20} more_` + : "") + + `\n\nReview with \`listDiscoveredActions\`, then \`approveApiSurface\` to proceed.`, + ); +} + +async function handleParseOpenApiSpec( + integrationName: string, + specSource: string, +): Promise { + const state = await loadState(integrationName); + if (!state) { + return { error: `Integration "${integrationName}" not found. Run startOnboarding first.` }; + } + + await updatePhase(integrationName, "discovery", { status: "in-progress" }); + + // Fetch the spec (URL or file path) + let specContent: string; + try { + if (specSource.startsWith("http://") || specSource.startsWith("https://")) { + const response = await fetch(specSource); + if (!response.ok) { + return { error: `Failed to fetch spec: ${response.status} ${response.statusText}` }; + } + specContent = await response.text(); + } else { + const fs = await import("fs/promises"); + specContent = await fs.readFile(specSource, "utf-8"); + } + } catch (err: any) { + return { error: `Failed to read spec from ${specSource}: ${err?.message ?? err}` }; + } + + let spec: any; + try { + spec = JSON.parse(specContent); + } catch { + try { + // Try YAML if JSON fails (basic line parsing) + return { error: "YAML specs not yet supported — please provide a JSON OpenAPI spec." }; + } catch { + return { error: "Could not parse spec as JSON or YAML." }; + } + } + + // Extract actions from OpenAPI paths + const actions: DiscoveredAction[] = []; + const paths = spec.paths ?? {}; + for (const [pathStr, pathItem] of Object.entries(paths) as [string, any][]) { + for (const method of ["get", "post", "put", "patch", "delete"] as const) { + const op = pathItem?.[method]; + if (!op) continue; + + const name = op.operationId ?? `${method}${pathStr.replace(/[^a-zA-Z0-9]/g, "_")}`; + const camelName = name.replace(/_([a-z])/g, (_: string, c: string) => c.toUpperCase()); + + const parameters: DiscoveredParameter[] = (op.parameters ?? []).map( + (p: any) => ({ + name: p.name, + type: p.schema?.type ?? "string", + description: p.description, + required: p.required ?? false, + }), + ); + + // Also include request body fields as parameters + const requestBody = op.requestBody?.content?.["application/json"]?.schema; + if (requestBody?.properties) { + for (const [propName, propSchema] of Object.entries(requestBody.properties) as [string, any][]) { + parameters.push({ + name: propName, + type: propSchema.type ?? "string", + description: propSchema.description, + required: requestBody.required?.includes(propName) ?? false, + }); + } + } + + actions.push({ + name: camelName, + description: op.summary ?? op.description ?? `${method.toUpperCase()} ${pathStr}`, + method: method.toUpperCase(), + path: pathStr, + parameters, + sourceUrl: specSource, + }); + } + } + + const surface: ApiSurface = { + integrationName, + discoveredAt: new Date().toISOString(), + source: specSource, + actions, + }; + + await writeArtifactJson(integrationName, "discovery", "api-surface.json", surface); + + return createActionResultFromMarkdownDisplay( + `## OpenAPI spec parsed: ${integrationName}\n\n` + + `**Source:** ${specSource}\n` + + `**OpenAPI version:** ${spec.openapi ?? spec.swagger ?? "unknown"}\n` + + `**Actions found:** ${actions.length}\n\n` + + actions + .slice(0, 20) + .map((a) => `- **${a.name}** (\`${a.method} ${a.path}\`): ${a.description}`) + .join("\n") + + (actions.length > 20 ? `\n\n_...and ${actions.length - 20} more_` : "") + + `\n\nReview with \`listDiscoveredActions\`, then \`approveApiSurface\` to proceed.`, + ); +} + +async function handleListDiscoveredActions( + integrationName: string, +): Promise { + const surface = await readArtifactJson( + integrationName, + "discovery", + "api-surface.json", + ); + if (!surface) { + return { error: `No discovered actions found for "${integrationName}". Run crawlDocUrl or parseOpenApiSpec first.` }; + } + + const lines = [ + `## Discovered actions: ${integrationName}`, + ``, + `**Source:** ${surface.source}`, + `**Discovered:** ${surface.discoveredAt}`, + `**Total actions:** ${surface.actions.length}`, + `**Status:** ${surface.approved ? "✅ Approved" : "⏳ Pending approval"}`, + ``, + `| # | Name | Description |`, + `|---|---|---|`, + ...surface.actions.map( + (a, i) => + `| ${i + 1} | \`${a.name}\` | ${a.description} |`, + ), + ]; + + return createActionResultFromMarkdownDisplay(lines.join("\n")); +} + +async function handleApproveApiSurface( + integrationName: string, + includeActions?: string[], + excludeActions?: string[], +): Promise { + const surface = await readArtifactJson( + integrationName, + "discovery", + "api-surface.json", + ); + if (!surface) { + return { error: `No discovered actions found for "${integrationName}".` }; + } + + let approved = surface.actions; + if (includeActions && includeActions.length > 0) { + approved = approved.filter((a) => includeActions.includes(a.name)); + } + if (excludeActions && excludeActions.length > 0) { + approved = approved.filter((a) => !excludeActions.includes(a.name)); + } + + const updated: ApiSurface = { + ...surface, + approved: true, + approvedAt: new Date().toISOString(), + approvedActions: approved.map((a) => a.name), + actions: approved, + }; + + await writeArtifactJson(integrationName, "discovery", "api-surface.json", updated); + await updatePhase(integrationName, "discovery", { status: "approved" }); + + return createActionResultFromMarkdownDisplay( + `## API surface approved: ${integrationName}\n\n` + + `**Approved actions:** ${approved.length}\n\n` + + approved.map((a) => `- \`${a.name}\`: ${a.description}`).join("\n") + + `\n\n**Next step:** Phase 2 — use \`generatePhrases\` to create natural language samples.`, + ); +} diff --git a/ts/packages/agents/onboarding/src/discovery/discoverySchema.agr b/ts/packages/agents/onboarding/src/discovery/discoverySchema.agr new file mode 100644 index 0000000000..f130d13d93 --- /dev/null +++ b/ts/packages/agents/onboarding/src/discovery/discoverySchema.agr @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Grammar for Phase 1 — Discovery actions. + +// crawlDocUrl - crawl API documentation at a URL + = crawl (docs | documentation | api) (at | from)? $(url:wildcard) for $(integrationName:wildcard) -> { + actionName: "crawlDocUrl", + parameters: { + url, + integrationName + } +} + | (fetch | scrape | read) (the)? $(integrationName:wildcard) (api)? (docs | documentation) (at | from)? $(url:wildcard) -> { + actionName: "crawlDocUrl", + parameters: { + integrationName, + url + } +}; + +// parseOpenApiSpec - parse an OpenAPI or Swagger spec + = parse (the)? (openapi | swagger | api) spec (at | from)? $(specSource:wildcard) for $(integrationName:wildcard) -> { + actionName: "parseOpenApiSpec", + parameters: { + specSource, + integrationName + } +} + | (load | ingest) (the)? $(integrationName:wildcard) (openapi | swagger | api) spec (from | at)? $(specSource:wildcard) -> { + actionName: "parseOpenApiSpec", + parameters: { + integrationName, + specSource + } +}; + +// listDiscoveredActions - show what was found + = list (discovered | found)? actions for $(integrationName:wildcard) -> { + actionName: "listDiscoveredActions", + parameters: { + integrationName + } +} + | (show | what are) (the)? (discovered | available)? actions (for | in)? $(integrationName:wildcard) -> { + actionName: "listDiscoveredActions", + parameters: { + integrationName + } +}; + +// approveApiSurface - lock in the discovered action set + = approve (the)? (api)? (surface | actions) for $(integrationName:wildcard) -> { + actionName: "approveApiSurface", + parameters: { + integrationName + } +} + | (confirm | finalize) (the)? $(integrationName:wildcard) (api)? surface -> { + actionName: "approveApiSurface", + parameters: { + integrationName + } +}; + +import { DiscoveryActions } from "./discoverySchema.ts"; + + : DiscoveryActions = + | + | + | ; diff --git a/ts/packages/agents/onboarding/src/discovery/discoverySchema.ts b/ts/packages/agents/onboarding/src/discovery/discoverySchema.ts new file mode 100644 index 0000000000..fdbaeddd10 --- /dev/null +++ b/ts/packages/agents/onboarding/src/discovery/discoverySchema.ts @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Phase 1 — Discovery: enumerate the API surface of the target application. +// Actions in this phase ingest documentation or API specs and produce +// a list of candidate actions saved to ~/.typeagent/onboarding//discovery/api-surface.json + +export type DiscoveryActions = + | CrawlDocUrlAction + | ParseOpenApiSpecAction + | ListDiscoveredActionsAction + | ApproveApiSurfaceAction; + +export type CrawlDocUrlAction = { + actionName: "crawlDocUrl"; + parameters: { + // Name of the integration being onboarded + integrationName: string; + // URL of the API documentation page to crawl (e.g. "https://api.slack.com/methods") + url: string; + // Maximum link-follow depth (default: 2) + maxDepth?: number; + }; +}; + +export type ParseOpenApiSpecAction = { + actionName: "parseOpenApiSpec"; + parameters: { + // Name of the integration being onboarded + integrationName: string; + // URL or absolute file path to the OpenAPI 3.x or Swagger 2.x spec + specSource: string; + }; +}; + +export type ListDiscoveredActionsAction = { + actionName: "listDiscoveredActions"; + parameters: { + // Integration name to list discovered actions for + integrationName: string; + }; +}; + +export type ApproveApiSurfaceAction = { + actionName: "approveApiSurface"; + parameters: { + // Integration name to approve + integrationName: string; + // If provided, only these action names are included in the approved surface (excludes all others) + includeActions?: string[]; + // Action names to exclude from the approved surface + excludeActions?: string[]; + }; +}; diff --git a/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts b/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts new file mode 100644 index 0000000000..2cb60ada15 --- /dev/null +++ b/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Phase 4 — Grammar Generation handler. +// Generates a .agr grammar file from the approved schema and phrase set, +// then compiles it via the action-grammar-compiler (agc) to validate. + +import { + ActionContext, + TypeAgentAction, + ActionResult, +} from "@typeagent/agent-sdk"; +import { + createActionResultFromTextDisplay, + createActionResultFromMarkdownDisplay, +} from "@typeagent/agent-sdk/helpers/action"; +import { GrammarGenActions } from "./grammarGenSchema.js"; +import { + loadState, + updatePhase, + writeArtifact, + readArtifact, + readArtifactJson, + getPhasePath, +} from "../lib/workspace.js"; +import { getGrammarGenModel } from "../lib/llm.js"; +import { ApiSurface } from "../discovery/discoveryHandler.js"; +import { PhraseSet } from "../phraseGen/phraseGenHandler.js"; +import { spawn } from "child_process"; +import path from "path"; + +export async function executeGrammarGenAction( + action: TypeAgentAction, + _context: ActionContext, +): Promise { + switch (action.actionName) { + case "generateGrammar": + return handleGenerateGrammar(action.parameters.integrationName); + case "compileGrammar": + return handleCompileGrammar(action.parameters.integrationName); + case "approveGrammar": + return handleApproveGrammar(action.parameters.integrationName); + } +} + +async function handleGenerateGrammar(integrationName: string): Promise { + const state = await loadState(integrationName); + if (!state) return { error: `Integration "${integrationName}" not found.` }; + if (state.phases.schemaGen.status !== "approved") { + return { error: `Schema phase must be approved first. Run approveSchema.` }; + } + + const surface = await readArtifactJson(integrationName, "discovery", "api-surface.json"); + const phraseSet = await readArtifactJson(integrationName, "phraseGen", "phrases.json"); + const schemaTs = await readArtifact(integrationName, "schemaGen", "schema.ts"); + if (!surface || !phraseSet || !schemaTs) { + return { error: `Missing required artifacts for "${integrationName}".` }; + } + + await updatePhase(integrationName, "grammarGen", { status: "in-progress" }); + + const model = getGrammarGenModel(); + const prompt = buildGrammarPrompt(integrationName, surface, phraseSet, schemaTs); + const result = await model.complete(prompt); + if (!result.success) { + return { error: `Grammar generation failed: ${result.message}` }; + } + + const grammarContent = extractGrammarContent(result.data); + await writeArtifact(integrationName, "grammarGen", "schema.agr", grammarContent); + + return createActionResultFromMarkdownDisplay( + `## Grammar generated: ${integrationName}\n\n` + + "```\n" + grammarContent.slice(0, 2000) + (grammarContent.length > 2000 ? "\n// ... (truncated)" : "") + "\n```\n\n" + + `Use \`compileGrammar\` to validate, or \`approveGrammar\` if it looks correct.`, + ); +} + +async function handleCompileGrammar(integrationName: string): Promise { + const grammarPath = path.join( + getPhasePath(integrationName, "grammarGen"), + "schema.agr", + ); + const outputPath = path.join( + getPhasePath(integrationName, "grammarGen"), + "schema.ag.json", + ); + + const grammarContent = await readArtifact(integrationName, "grammarGen", "schema.agr"); + if (!grammarContent) { + return { error: `No grammar file found for "${integrationName}". Run generateGrammar first.` }; + } + + return new Promise((resolve) => { + const proc = spawn("agc", ["-i", grammarPath, "-o", outputPath], { + stdio: ["ignore", "pipe", "pipe"], + }); + + let stdout = ""; + let stderr = ""; + proc.stdout?.on("data", (d: Buffer) => { stdout += d.toString(); }); + proc.stderr?.on("data", (d: Buffer) => { stderr += d.toString(); }); + + proc.on("close", (code) => { + if (code === 0) { + resolve( + createActionResultFromMarkdownDisplay( + `## Grammar compiled successfully: ${integrationName}\n\n` + + `Output: \`schema.ag.json\`\n\n` + + (stdout ? `Compiler output:\n\`\`\`\n${stdout}\n\`\`\`` : "") + + `\n\nUse \`approveGrammar\` to proceed to scaffolding.`, + ), + ); + } else { + resolve({ + error: + `Grammar compilation failed (exit code ${code}).\n\n` + + (stderr || stdout || "No output from compiler.") + + `\n\nUse \`generateGrammar\` or \`refineSchema\` to fix the grammar.`, + }); + } + }); + + proc.on("error", (err) => { + resolve({ error: `Failed to run agc: ${err.message}. Is action-grammar-compiler installed?` }); + }); + }); +} + +async function handleApproveGrammar(integrationName: string): Promise { + const grammar = await readArtifact(integrationName, "grammarGen", "schema.agr"); + if (!grammar) { + return { error: `No grammar found for "${integrationName}". Run generateGrammar first.` }; + } + + await updatePhase(integrationName, "grammarGen", { status: "approved" }); + + return createActionResultFromMarkdownDisplay( + `## Grammar approved: ${integrationName}\n\n` + + `**Next step:** Phase 5 — use \`scaffoldAgent\` to create the agent package.`, + ); +} + +function buildGrammarPrompt( + integrationName: string, + surface: ApiSurface, + phraseSet: PhraseSet, + schemaTs: string, +): { role: "system" | "user"; content: string }[] { + const actionExamples = surface.actions + .map((a) => { + const phrases = phraseSet.phrases[a.name] ?? []; + return `Action: ${a.name}\nPhrases:\n${phrases.slice(0, 4).map((p) => ` - "${p}"`).join("\n")}`; + }) + .join("\n\n"); + + return [ + { + role: "system", + content: + "You are an expert in TypeAgent grammar files (.agr format). " + + "Grammar rules use this syntax:\n" + + " = pattern -> { actionName: \"name\", parameters: { ... } }\n" + + " | alternative -> { ... };\n\n" + + "Pattern syntax:\n" + + " - $(paramName:wildcard) captures 1+ words\n" + + " - $(paramName:word) captures exactly 1 word\n" + + " - (optional)? makes tokens optional\n" + + " - word matches a literal word\n" + + " - | separates alternatives\n\n" + + "The file must end with:\n" + + " import { ActionType } from \"./schemaFile.ts\";\n" + + " : ActionType = | | ...;\n\n" + + "Return only the .agr file content.", + }, + { + role: "user", + content: + `Generate a TypeAgent .agr grammar file for the "${integrationName}" integration.\n\n` + + `TypeScript schema:\n\`\`\`typescript\n${schemaTs.slice(0, 3000)}\n\`\`\`\n\n` + + `Sample phrases for each action:\n${actionExamples}\n\n` + + `The schema file will be imported as "./schema.ts". The entry type is the main union type from the schema.`, + }, + ]; +} + +function extractGrammarContent(llmResponse: string): string { + const fenceMatch = llmResponse.match(/```(?:agr)?\n([\s\S]*?)```/); + if (fenceMatch) return fenceMatch[1].trim(); + return llmResponse.trim(); +} diff --git a/ts/packages/agents/onboarding/src/grammarGen/grammarGenSchema.agr b/ts/packages/agents/onboarding/src/grammarGen/grammarGenSchema.agr new file mode 100644 index 0000000000..49b3b55a76 --- /dev/null +++ b/ts/packages/agents/onboarding/src/grammarGen/grammarGenSchema.agr @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Grammar for Phase 4 — Grammar Generation actions. + + = generate (the)? (agr)? grammar for $(integrationName:wildcard) -> { + actionName: "generateGrammar", + parameters: { + integrationName + } +} + | (create | produce | write) (the)? $(integrationName:wildcard) (agr)? grammar (file)? -> { + actionName: "generateGrammar", + parameters: { + integrationName + } +}; + + = compile (the)? $(integrationName:wildcard) grammar -> { + actionName: "compileGrammar", + parameters: { + integrationName + } +} + | (validate | build | check) (the)? $(integrationName:wildcard) grammar -> { + actionName: "compileGrammar", + parameters: { + integrationName + } +}; + + = approve (the)? $(integrationName:wildcard) grammar -> { + actionName: "approveGrammar", + parameters: { + integrationName + } +} + | (confirm | finalize) (the)? $(integrationName:wildcard) (agr)? grammar -> { + actionName: "approveGrammar", + parameters: { + integrationName + } +}; + +import { GrammarGenActions } from "./grammarGenSchema.ts"; + + : GrammarGenActions = + | + | ; diff --git a/ts/packages/agents/onboarding/src/grammarGen/grammarGenSchema.ts b/ts/packages/agents/onboarding/src/grammarGen/grammarGenSchema.ts new file mode 100644 index 0000000000..b0b9e91757 --- /dev/null +++ b/ts/packages/agents/onboarding/src/grammarGen/grammarGenSchema.ts @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Phase 4 — Grammar Generation: produce a .agr grammar file from the +// approved TypeScript schema and sample phrases, then compile and validate it. + +export type GrammarGenActions = + | GenerateGrammarAction + | CompileGrammarAction + | ApproveGrammarAction; + +export type GenerateGrammarAction = { + actionName: "generateGrammar"; + parameters: { + // Integration name to generate grammar for + integrationName: string; + }; +}; + +export type CompileGrammarAction = { + actionName: "compileGrammar"; + parameters: { + // Integration name whose grammar to compile and validate + integrationName: string; + }; +}; + +export type ApproveGrammarAction = { + actionName: "approveGrammar"; + parameters: { + // Integration name to approve grammar for + integrationName: string; + }; +}; diff --git a/ts/packages/agents/onboarding/src/lib/llm.ts b/ts/packages/agents/onboarding/src/lib/llm.ts new file mode 100644 index 0000000000..d5aae8c7d0 --- /dev/null +++ b/ts/packages/agents/onboarding/src/lib/llm.ts @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// aiclient ChatModel factories for each onboarding phase. +// Each phase gets a distinct debug tag so LLM calls are easy to trace +// with DEBUG=typeagent:openai:* environment variable. +// +// Credentials are read from ts/.env via the standard TypeAgent mechanism. + +import { createChatModelDefault } from "aiclient"; +import type { ChatModel } from "aiclient"; + +export function getDiscoveryModel(): ChatModel { + return createChatModelDefault("onboarding:discovery"); +} + +export function getPhraseGenModel(): ChatModel { + return createChatModelDefault("onboarding:phrasegen"); +} + +export function getSchemaGenModel(): ChatModel { + return createChatModelDefault("onboarding:schemagen"); +} + +export function getGrammarGenModel(): ChatModel { + return createChatModelDefault("onboarding:grammargen"); +} + +export function getTestingModel(): ChatModel { + return createChatModelDefault("onboarding:testing"); +} + +export function getPackagingModel(): ChatModel { + return createChatModelDefault("onboarding:packaging"); +} diff --git a/ts/packages/agents/onboarding/src/lib/workspace.ts b/ts/packages/agents/onboarding/src/lib/workspace.ts new file mode 100644 index 0000000000..1b76879e8c --- /dev/null +++ b/ts/packages/agents/onboarding/src/lib/workspace.ts @@ -0,0 +1,214 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Manages per-integration workspace state persisted to disk. +// Each integration gets a folder at ~/.typeagent/onboarding// +// containing state.json and phase-specific artifact subdirectories. + +import fs from "fs/promises"; +import path from "path"; +import os from "os"; + +export type PhaseStatus = "pending" | "in-progress" | "approved" | "skipped"; + +export type PhaseState = { + status: PhaseStatus; + startedAt?: string; + completedAt?: string; +}; + +export type OnboardingPhase = + | "discovery" + | "phraseGen" + | "schemaGen" + | "grammarGen" + | "scaffolder" + | "testing" + | "packaging"; + +export const PHASE_ORDER: OnboardingPhase[] = [ + "discovery", + "phraseGen", + "schemaGen", + "grammarGen", + "scaffolder", + "testing", + "packaging", +]; + +export type OnboardingConfig = { + integrationName: string; + description?: string; + apiType?: "rest" | "graphql" | "websocket" | "ipc" | "sdk"; + docSources?: string[]; +}; + +export type OnboardingState = { + integrationName: string; + createdAt: string; + updatedAt: string; + // "complete" when all phases are approved + currentPhase: OnboardingPhase | "complete"; + config: OnboardingConfig; + phases: Record; +}; + +const BASE_DIR = path.join(os.homedir(), ".typeagent", "onboarding"); + +export function getWorkspacePath(integrationName: string): string { + return path.join(BASE_DIR, integrationName); +} + +export function getPhasePath( + integrationName: string, + phase: OnboardingPhase, +): string { + return path.join(getWorkspacePath(integrationName), phase); +} + +export async function createWorkspace( + config: OnboardingConfig, +): Promise { + const workspacePath = getWorkspacePath(config.integrationName); + await fs.mkdir(workspacePath, { recursive: true }); + + const emptyPhase = (): PhaseState => ({ status: "pending" }); + + const state: OnboardingState = { + integrationName: config.integrationName, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + currentPhase: "discovery", + config, + phases: { + discovery: emptyPhase(), + phraseGen: emptyPhase(), + schemaGen: emptyPhase(), + grammarGen: emptyPhase(), + scaffolder: emptyPhase(), + testing: emptyPhase(), + packaging: emptyPhase(), + }, + }; + + // Create phase subdirectories up front + for (const phase of PHASE_ORDER) { + await fs.mkdir(path.join(workspacePath, phase), { recursive: true }); + } + + await saveState(state); + return state; +} + +export async function loadState( + integrationName: string, +): Promise { + const statePath = path.join( + getWorkspacePath(integrationName), + "state.json", + ); + try { + const content = await fs.readFile(statePath, "utf-8"); + return JSON.parse(content) as OnboardingState; + } catch { + return undefined; + } +} + +export async function saveState(state: OnboardingState): Promise { + state.updatedAt = new Date().toISOString(); + const statePath = path.join( + getWorkspacePath(state.integrationName), + "state.json", + ); + await fs.writeFile(statePath, JSON.stringify(state, null, 2), "utf-8"); +} + +export async function updatePhase( + integrationName: string, + phase: OnboardingPhase, + update: Partial, +): Promise { + const state = await loadState(integrationName); + if (!state) { + throw new Error(`Integration "${integrationName}" not found`); + } + state.phases[phase] = { ...state.phases[phase], ...update }; + + // When approved, advance currentPhase to the next phase + if (update.status === "approved") { + state.phases[phase].completedAt = new Date().toISOString(); + const idx = PHASE_ORDER.indexOf(phase); + if (idx >= 0 && idx < PHASE_ORDER.length - 1) { + state.currentPhase = PHASE_ORDER[idx + 1]; + } else if (idx === PHASE_ORDER.length - 1) { + state.currentPhase = "complete"; + } + } + + if (update.status === "in-progress" && !state.phases[phase].startedAt) { + state.phases[phase].startedAt = new Date().toISOString(); + } + + await saveState(state); + return state; +} + +export async function readArtifact( + integrationName: string, + phase: OnboardingPhase, + filename: string, +): Promise { + const filePath = path.join(getPhasePath(integrationName, phase), filename); + try { + return await fs.readFile(filePath, "utf-8"); + } catch { + return undefined; + } +} + +export async function writeArtifact( + integrationName: string, + phase: OnboardingPhase, + filename: string, + content: string, +): Promise { + const dirPath = getPhasePath(integrationName, phase); + await fs.mkdir(dirPath, { recursive: true }); + const filePath = path.join(dirPath, filename); + await fs.writeFile(filePath, content, "utf-8"); + return filePath; +} + +export async function readArtifactJson( + integrationName: string, + phase: OnboardingPhase, + filename: string, +): Promise { + const content = await readArtifact(integrationName, phase, filename); + if (!content) return undefined; + return JSON.parse(content) as T; +} + +export async function writeArtifactJson( + integrationName: string, + phase: OnboardingPhase, + filename: string, + data: unknown, +): Promise { + return writeArtifact( + integrationName, + phase, + filename, + JSON.stringify(data, null, 2), + ); +} + +export async function listIntegrations(): Promise { + try { + const entries = await fs.readdir(BASE_DIR, { withFileTypes: true }); + return entries.filter((e) => e.isDirectory()).map((e) => e.name); + } catch { + return []; + } +} diff --git a/ts/packages/agents/onboarding/src/onboardingActionHandler.ts b/ts/packages/agents/onboarding/src/onboardingActionHandler.ts new file mode 100644 index 0000000000..823fa07e51 --- /dev/null +++ b/ts/packages/agents/onboarding/src/onboardingActionHandler.ts @@ -0,0 +1,285 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { + ActionContext, + AppAgent, + TypeAgentAction, + ActionResult, +} from "@typeagent/agent-sdk"; +import { + createActionResultFromTextDisplay, + createActionResultFromMarkdownDisplay, +} from "@typeagent/agent-sdk/helpers/action"; +import { OnboardingActions } from "./onboardingSchema.js"; +import { DiscoveryActions } from "./discovery/discoverySchema.js"; +import { PhraseGenActions } from "./phraseGen/phraseGenSchema.js"; +import { SchemaGenActions } from "./schemaGen/schemaGenSchema.js"; +import { GrammarGenActions } from "./grammarGen/grammarGenSchema.js"; +import { ScaffolderActions } from "./scaffolder/scaffolderSchema.js"; +import { TestingActions } from "./testing/testingSchema.js"; +import { PackagingActions } from "./packaging/packagingSchema.js"; +import { executeDiscoveryAction } from "./discovery/discoveryHandler.js"; +import { executePhraseGenAction } from "./phraseGen/phraseGenHandler.js"; +import { executeSchemaGenAction } from "./schemaGen/schemaGenHandler.js"; +import { executeGrammarGenAction } from "./grammarGen/grammarGenHandler.js"; +import { executeScaffolderAction } from "./scaffolder/scaffolderHandler.js"; +import { executeTestingAction } from "./testing/testingHandler.js"; +import { executePackagingAction } from "./packaging/packagingHandler.js"; +import { + createWorkspace, + loadState, + listIntegrations, +} from "./lib/workspace.js"; + +type AllActions = + | OnboardingActions + | DiscoveryActions + | PhraseGenActions + | SchemaGenActions + | GrammarGenActions + | ScaffolderActions + | TestingActions + | PackagingActions; + +export function instantiate(): AppAgent { + return { + executeAction, + }; +} + +async function executeAction( + action: TypeAgentAction, + context: ActionContext, +): Promise { + const { actionName } = action as TypeAgentAction; + + // Top-level coordination actions + if ( + actionName === "startOnboarding" || + actionName === "resumeOnboarding" || + actionName === "getOnboardingStatus" || + actionName === "listIntegrations" + ) { + return executeOnboardingAction( + action as TypeAgentAction, + context, + ); + } + + // Discovery phase + if ( + actionName === "crawlDocUrl" || + actionName === "parseOpenApiSpec" || + actionName === "listDiscoveredActions" || + actionName === "approveApiSurface" + ) { + return executeDiscoveryAction( + action as TypeAgentAction, + context, + ); + } + + // Phrase generation phase + if ( + actionName === "generatePhrases" || + actionName === "addPhrase" || + actionName === "removePhrase" || + actionName === "approvePhrases" + ) { + return executePhraseGenAction( + action as TypeAgentAction, + context, + ); + } + + // Schema generation phase + if ( + actionName === "generateSchema" || + actionName === "refineSchema" || + actionName === "approveSchema" + ) { + return executeSchemaGenAction( + action as TypeAgentAction, + context, + ); + } + + // Grammar generation phase + if ( + actionName === "generateGrammar" || + actionName === "compileGrammar" || + actionName === "approveGrammar" + ) { + return executeGrammarGenAction( + action as TypeAgentAction, + context, + ); + } + + // Scaffolder phase + if ( + actionName === "scaffoldAgent" || + actionName === "scaffoldPlugin" || + actionName === "listTemplates" + ) { + return executeScaffolderAction( + action as TypeAgentAction, + context, + ); + } + + // Testing phase + if ( + actionName === "generateTests" || + actionName === "runTests" || + actionName === "getTestResults" || + actionName === "proposeRepair" || + actionName === "approveRepair" + ) { + return executeTestingAction( + action as TypeAgentAction, + context, + ); + } + + // Packaging phase + if (actionName === "packageAgent" || actionName === "validatePackage") { + return executePackagingAction( + action as TypeAgentAction, + context, + ); + } + + return { error: `Unknown action: ${actionName}` }; +} + +async function executeOnboardingAction( + action: TypeAgentAction, + _context: ActionContext, +): Promise { + switch (action.actionName) { + case "startOnboarding": { + const { integrationName, description, apiType } = action.parameters; + const existing = await loadState(integrationName); + if (existing) { + return createActionResultFromTextDisplay( + `Integration "${integrationName}" already exists (current phase: ${existing.currentPhase}). Use resumeOnboarding to continue.`, + ); + } + const state = await createWorkspace({ + integrationName, + description, + apiType, + }); + return createActionResultFromMarkdownDisplay( + `## Onboarding started: ${integrationName}\n\n` + + `**Next step:** Phase 1 — Discovery\n\n` + + `Use \`crawlDocUrl\` or \`parseOpenApiSpec\` to enumerate the API surface.\n\n` + + `Workspace: \`~/.typeagent/onboarding/${integrationName}/\``, + ); + } + + case "resumeOnboarding": { + const { integrationName, fromPhase } = action.parameters; + const state = await loadState(integrationName); + if (!state) { + return { + error: `Integration "${integrationName}" not found. Use startOnboarding to create it.`, + }; + } + const phase = fromPhase ?? state.currentPhase; + return createActionResultFromMarkdownDisplay( + `## Resuming: ${integrationName}\n\n` + + `**Current phase:** ${phase}\n\n` + + `${phaseNextStepHint(phase)}`, + ); + } + + case "getOnboardingStatus": { + const { integrationName } = action.parameters; + const state = await loadState(integrationName); + if (!state) { + return { + error: `Integration "${integrationName}" not found.`, + }; + } + const lines = [ + `## ${integrationName} — Onboarding Status`, + ``, + `**Current phase:** ${state.currentPhase}`, + `**Started:** ${state.createdAt}`, + `**Updated:** ${state.updatedAt}`, + ``, + `| Phase | Status |`, + `|---|---|`, + ...Object.entries(state.phases).map( + ([phase, ps]) => + `| ${phase} | ${statusIcon(ps.status)} ${ps.status} |`, + ), + ]; + return createActionResultFromMarkdownDisplay(lines.join("\n")); + } + + case "listIntegrations": { + const { status } = action.parameters; + const names = await listIntegrations(); + if (names.length === 0) { + return createActionResultFromTextDisplay( + "No integrations found. Use startOnboarding to begin.", + ); + } + const lines = [`## Integrations`, ``]; + for (const name of names) { + const state = await loadState(name); + if (!state) continue; + if (status === "complete" && state.currentPhase !== "complete") + continue; + if ( + status === "in-progress" && + state.currentPhase === "complete" + ) + continue; + lines.push( + `- **${name}** — ${state.currentPhase} (updated ${state.updatedAt})`, + ); + } + return createActionResultFromMarkdownDisplay(lines.join("\n")); + } + } +} + +function phaseNextStepHint(phase: string): string { + const hints: Record = { + discovery: + "Use `crawlDocUrl` or `parseOpenApiSpec` to enumerate the API surface.", + phraseGen: + "Use `generatePhrases` to create natural language samples for each action.", + schemaGen: + "Use `generateSchema` to produce the TypeScript action schema.", + grammarGen: + "Use `generateGrammar` to produce the .agr grammar file, then `compileGrammar` to validate.", + scaffolder: + "Use `scaffoldAgent` to stamp out the agent package infrastructure.", + testing: + "Use `generateTests` then `runTests` to validate phrase-to-action mapping.", + packaging: "Use `packageAgent` to prepare the agent for distribution.", + complete: "Onboarding is complete.", + }; + return hints[phase] ?? ""; +} + +function statusIcon(status: string): string { + switch (status) { + case "pending": + return "⏳"; + case "in-progress": + return "🔄"; + case "approved": + return "✅"; + case "skipped": + return "⏭️"; + default: + return "❓"; + } +} diff --git a/ts/packages/agents/onboarding/src/onboardingManifest.json b/ts/packages/agents/onboarding/src/onboardingManifest.json new file mode 100644 index 0000000000..e699db6946 --- /dev/null +++ b/ts/packages/agents/onboarding/src/onboardingManifest.json @@ -0,0 +1,77 @@ +{ + "emojiChar": "🛠️", + "description": "Agent for onboarding new applications and APIs into TypeAgent", + "defaultEnabled": false, + "schema": { + "description": "Top-level onboarding coordination: start, resume, and check status of integration onboarding workflows", + "originalSchemaFile": "./onboardingSchema.ts", + "schemaFile": "../dist/onboardingSchema.pas.json", + "grammarFile": "../dist/onboardingSchema.ag.json", + "schemaType": "OnboardingActions" + }, + "subActionManifests": { + "onboarding-discovery": { + "schema": { + "description": "Phase 1: Enumerate the API surface of the target application by crawling documentation or parsing an OpenAPI spec", + "originalSchemaFile": "./discovery/discoverySchema.ts", + "schemaFile": "../dist/discoverySchema.pas.json", + "grammarFile": "../dist/discoverySchema.ag.json", + "schemaType": "DiscoveryActions" + } + }, + "onboarding-phrasegen": { + "schema": { + "description": "Phase 2: Generate natural language sample phrases that users would say to invoke each discovered action", + "originalSchemaFile": "./phraseGen/phraseGenSchema.ts", + "schemaFile": "../dist/phraseGenSchema.pas.json", + "grammarFile": "../dist/phraseGenSchema.ag.json", + "schemaType": "PhraseGenActions" + } + }, + "onboarding-schemagen": { + "schema": { + "description": "Phase 3: Generate TypeScript action schema types with comments that map user requests to the target API surface", + "originalSchemaFile": "./schemaGen/schemaGenSchema.ts", + "schemaFile": "../dist/schemaGenSchema.pas.json", + "grammarFile": "../dist/schemaGenSchema.ag.json", + "schemaType": "SchemaGenActions" + } + }, + "onboarding-grammargen": { + "schema": { + "description": "Phase 4: Generate .agr grammar files from action schemas and sample phrases, then compile and validate them", + "originalSchemaFile": "./grammarGen/grammarGenSchema.ts", + "schemaFile": "../dist/grammarGenSchema.pas.json", + "grammarFile": "../dist/grammarGenSchema.ag.json", + "schemaType": "GrammarGenActions" + } + }, + "onboarding-scaffolder": { + "schema": { + "description": "Phase 5: Scaffold the complete TypeAgent agent package infrastructure including manifest, handler, package.json, and any required plugins", + "originalSchemaFile": "./scaffolder/scaffolderSchema.ts", + "schemaFile": "../dist/scaffolderSchema.pas.json", + "grammarFile": "../dist/scaffolderSchema.ag.json", + "schemaType": "ScaffolderActions" + } + }, + "onboarding-testing": { + "schema": { + "description": "Phase 6: Generate test cases from sample phrases and run a phrase-to-action validation loop, proposing schema and grammar repairs for failures", + "originalSchemaFile": "./testing/testingSchema.ts", + "schemaFile": "../dist/testingSchema.pas.json", + "grammarFile": "../dist/testingSchema.ag.json", + "schemaType": "TestingActions" + } + }, + "onboarding-packaging": { + "schema": { + "description": "Phase 7: Package the completed agent for distribution and register it with the TypeAgent dispatcher", + "originalSchemaFile": "./packaging/packagingSchema.ts", + "schemaFile": "../dist/packagingSchema.pas.json", + "grammarFile": "../dist/packagingSchema.ag.json", + "schemaType": "PackagingActions" + } + } + } +} diff --git a/ts/packages/agents/onboarding/src/onboardingSchema.agr b/ts/packages/agents/onboarding/src/onboardingSchema.agr new file mode 100644 index 0000000000..a18f480ecc --- /dev/null +++ b/ts/packages/agents/onboarding/src/onboardingSchema.agr @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Grammar for top-level onboarding coordination actions. + +// startOnboarding - begin a new integration onboarding workflow + = start onboarding (for)? $(integrationName:wildcard) -> { + actionName: "startOnboarding", + parameters: { + integrationName + } +} + | onboard $(integrationName:wildcard) (into TypeAgent)? -> { + actionName: "startOnboarding", + parameters: { + integrationName + } +} + | (begin | create) (a)? (new)? $(integrationName:wildcard) integration -> { + actionName: "startOnboarding", + parameters: { + integrationName + } +}; + +// resumeOnboarding - continue an in-progress onboarding + = resume onboarding (for)? $(integrationName:wildcard) -> { + actionName: "resumeOnboarding", + parameters: { + integrationName + } +} + | continue (the)? $(integrationName:wildcard) onboarding -> { + actionName: "resumeOnboarding", + parameters: { + integrationName + } +}; + +// getOnboardingStatus - check the current phase and status + = (what's | what is) (the)? status (of)? (the)? $(integrationName:wildcard) onboarding -> { + actionName: "getOnboardingStatus", + parameters: { + integrationName + } +} + | show (me)? (the)? $(integrationName:wildcard) onboarding status -> { + actionName: "getOnboardingStatus", + parameters: { + integrationName + } +} + | how far along is (the)? $(integrationName:wildcard) (onboarding)? -> { + actionName: "getOnboardingStatus", + parameters: { + integrationName + } +}; + +// listIntegrations - list all known integrations + = list (all)? (my)? integrations -> { + actionName: "listIntegrations", + parameters: {} +} + | show (me)? (all)? (my)? integrations -> { + actionName: "listIntegrations", + parameters: {} +} + | what integrations (do I have | are there)? -> { + actionName: "listIntegrations", + parameters: {} +}; + +import { OnboardingActions } from "./onboardingSchema.ts"; + + : OnboardingActions = + | + | + | ; diff --git a/ts/packages/agents/onboarding/src/onboardingSchema.ts b/ts/packages/agents/onboarding/src/onboardingSchema.ts new file mode 100644 index 0000000000..7be5badfa4 --- /dev/null +++ b/ts/packages/agents/onboarding/src/onboardingSchema.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Top-level onboarding coordination actions. +// These actions manage the lifecycle of an integration onboarding workflow. + +export type OnboardingActions = + | StartOnboardingAction + | ResumeOnboardingAction + | GetOnboardingStatusAction + | ListIntegrationsAction; + +export type StartOnboardingAction = { + actionName: "startOnboarding"; + parameters: { + // Unique name for this integration (e.g. "slack", "jira", "my-rest-api"). + // Used as the workspace folder name — lowercase, no spaces. + integrationName: string; + // Human-readable description of what the integration does + description?: string; + // The type of API being integrated; helps select appropriate templates and bridge patterns + apiType?: "rest" | "graphql" | "websocket" | "ipc" | "sdk"; + }; +}; + +export type ResumeOnboardingAction = { + actionName: "resumeOnboarding"; + parameters: { + // Name of the integration to resume + integrationName: string; + // Optional: override which phase to start from (defaults to current phase in state.json) + fromPhase?: + | "discovery" + | "phraseGen" + | "schemaGen" + | "grammarGen" + | "scaffolder" + | "testing" + | "packaging"; + }; +}; + +export type GetOnboardingStatusAction = { + actionName: "getOnboardingStatus"; + parameters: { + // Integration name to check status for + integrationName: string; + }; +}; + +export type ListIntegrationsAction = { + actionName: "listIntegrations"; + parameters: { + // Filter by phase status; omit to list all + status?: "in-progress" | "complete"; + }; +}; diff --git a/ts/packages/agents/onboarding/src/packaging/packagingHandler.ts b/ts/packages/agents/onboarding/src/packaging/packagingHandler.ts new file mode 100644 index 0000000000..cb584f50a7 --- /dev/null +++ b/ts/packages/agents/onboarding/src/packaging/packagingHandler.ts @@ -0,0 +1,252 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Phase 7 — Packaging handler. +// Builds the scaffolded agent package and optionally registers it +// with the local TypeAgent dispatcher configuration. + +import { + ActionContext, + TypeAgentAction, + ActionResult, +} from "@typeagent/agent-sdk"; +import { + createActionResultFromTextDisplay, + createActionResultFromMarkdownDisplay, +} from "@typeagent/agent-sdk/helpers/action"; +import { PackagingActions } from "./packagingSchema.js"; +import { + loadState, + updatePhase, + readArtifact, + writeArtifactJson, + getWorkspacePath, +} from "../lib/workspace.js"; +import { spawn } from "child_process"; +import path from "path"; +import fs from "fs/promises"; +import os from "os"; + +export async function executePackagingAction( + action: TypeAgentAction, + _context: ActionContext, +): Promise { + switch (action.actionName) { + case "packageAgent": + return handlePackageAgent( + action.parameters.integrationName, + action.parameters.register ?? false, + ); + case "validatePackage": + return handleValidatePackage(action.parameters.integrationName); + } +} + +async function handlePackageAgent( + integrationName: string, + register: boolean, +): Promise { + const state = await loadState(integrationName); + if (!state) return { error: `Integration "${integrationName}" not found.` }; + if (state.phases.testing.status !== "approved") { + return { error: `Testing phase must be approved before packaging.` }; + } + + // Find where the scaffolded agent lives + const scaffoldedTo = await readArtifact( + integrationName, + "scaffolder", + "scaffolded-to.txt", + ); + if (!scaffoldedTo) { + return { error: `No scaffolded agent found. Run scaffoldAgent first.` }; + } + + const agentDir = scaffoldedTo.trim(); + + await updatePhase(integrationName, "packaging", { status: "in-progress" }); + + // Run pnpm install + build in the agent directory + const installResult = await runCommand("pnpm", ["install"], agentDir); + if (!installResult.success) { + return { + error: `pnpm install failed:\n${installResult.output}`, + }; + } + + const buildResult = await runCommand("pnpm", ["run", "build"], agentDir); + if (!buildResult.success) { + return { + error: `Build failed:\n${buildResult.output}`, + }; + } + + const summary = [ + `## Package built: ${integrationName}`, + ``, + `**Agent directory:** \`${agentDir}\``, + `**Build output:** \`${path.join(agentDir, "dist")}\``, + ``, + buildResult.output ? `\`\`\`\n${buildResult.output.slice(0, 500)}\n\`\`\`` : "", + ]; + + if (register) { + const registerResult = await registerWithDispatcher(integrationName, agentDir); + summary.push(``, registerResult); + } + + await updatePhase(integrationName, "packaging", { status: "approved" }); + + summary.push( + ``, + `**Onboarding complete!** 🎉`, + ``, + `The \`${integrationName}\` agent is ready for end-user testing.`, + register + ? `It has been registered with the local TypeAgent dispatcher.` + : `Run with \`register: true\` to register with the local dispatcher, or add it manually to \`ts/packages/defaultAgentProvider/data/config.json\`.`, + ); + + return createActionResultFromMarkdownDisplay(summary.join("\n")); +} + +async function handleValidatePackage(integrationName: string): Promise { + const scaffoldedTo = await readArtifact( + integrationName, + "scaffolder", + "scaffolded-to.txt", + ); + if (!scaffoldedTo) { + return { error: `No scaffolded agent found. Run scaffoldAgent first.` }; + } + + const agentDir = scaffoldedTo.trim(); + const checks: { name: string; passed: boolean; detail?: string }[] = []; + + // Check required files exist + const requiredFiles = [ + "package.json", + "tsconfig.json", + "src/tsconfig.json", + ]; + for (const file of requiredFiles) { + const exists = await fileExists(path.join(agentDir, file)); + checks.push({ name: `File: ${file}`, passed: exists }); + } + + // Check package.json exports + try { + const pkgJson = JSON.parse( + await fs.readFile(path.join(agentDir, "package.json"), "utf-8"), + ); + const hasManifestExport = + !!pkgJson.exports?.["./agent/manifest"]; + const hasHandlerExport = + !!pkgJson.exports?.["./agent/handlers"]; + checks.push({ + name: "package.json: exports ./agent/manifest", + passed: hasManifestExport, + }); + checks.push({ + name: "package.json: exports ./agent/handlers", + passed: hasHandlerExport, + }); + } catch { + checks.push({ + name: "package.json: parse", + passed: false, + detail: "Could not read package.json", + }); + } + + // Check dist exists (agent has been built) + const distExists = await fileExists(path.join(agentDir, "dist")); + checks.push({ name: "dist/ directory exists (built)", passed: distExists }); + + const passed = checks.filter((c) => c.passed).length; + const failed = checks.filter((c) => !c.passed).length; + + const lines = [ + `## Package validation: ${integrationName}`, + ``, + `**Passed:** ${passed} / ${checks.length}`, + ``, + ...checks.map( + (c) => + `${c.passed ? "✅" : "❌"} ${c.name}${c.detail ? ` — ${c.detail}` : ""}`, + ), + ]; + + if (failed === 0) { + lines.push(``, `Package is valid and ready for distribution.`); + } else { + lines.push(``, `Fix the failing checks above before packaging.`); + } + + return createActionResultFromMarkdownDisplay(lines.join("\n")); +} + +async function registerWithDispatcher( + integrationName: string, + agentDir: string, +): Promise { + // Add agent to defaultAgentProvider config.json + const configPath = path.resolve( + agentDir, + "../../../../defaultAgentProvider/data/config.json", + ); + + try { + const configRaw = await fs.readFile(configPath, "utf-8"); + const config = JSON.parse(configRaw); + + if (!config.agents) config.agents = {}; + if (config.agents[integrationName]) { + return `Agent "${integrationName}" is already registered in the dispatcher config.`; + } + + config.agents[integrationName] = { + name: `${integrationName}-agent`, + }; + + await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8"); + return `✅ Registered "${integrationName}" in dispatcher config at \`${configPath}\`\n\nRestart TypeAgent to load the new agent.`; + } catch (err: any) { + return `⚠️ Could not auto-register — update dispatcher config manually.\n${err?.message ?? err}`; + } +} + +async function runCommand( + cmd: string, + args: string[], + cwd: string, +): Promise<{ success: boolean; output: string }> { + return new Promise((resolve) => { + const proc = spawn(cmd, args, { + cwd, + stdio: ["ignore", "pipe", "pipe"], + shell: process.platform === "win32", + }); + + let output = ""; + proc.stdout?.on("data", (d: Buffer) => { output += d.toString(); }); + proc.stderr?.on("data", (d: Buffer) => { output += d.toString(); }); + + proc.on("close", (code) => { + resolve({ success: code === 0, output }); + }); + + proc.on("error", (err) => { + resolve({ success: false, output: err.message }); + }); + }); +} + +async function fileExists(p: string): Promise { + try { + await fs.access(p); + return true; + } catch { + return false; + } +} diff --git a/ts/packages/agents/onboarding/src/packaging/packagingSchema.agr b/ts/packages/agents/onboarding/src/packaging/packagingSchema.agr new file mode 100644 index 0000000000..f52b8cc9c3 --- /dev/null +++ b/ts/packages/agents/onboarding/src/packaging/packagingSchema.agr @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Grammar for Phase 7 — Packaging actions. + + = package (the)? $(integrationName:wildcard) agent -> { + actionName: "packageAgent", + parameters: { + integrationName + } +} + | (build | bundle | prepare) (the)? $(integrationName:wildcard) (agent)? (package)? (for distribution)? -> { + actionName: "packageAgent", + parameters: { + integrationName + } +}; + + = validate (the)? $(integrationName:wildcard) (agent)? package -> { + actionName: "validatePackage", + parameters: { + integrationName + } +} + | (check | verify) (the)? $(integrationName:wildcard) (agent)? package -> { + actionName: "validatePackage", + parameters: { + integrationName + } +}; + +import { PackagingActions } from "./packagingSchema.ts"; + + : PackagingActions = + | ; diff --git a/ts/packages/agents/onboarding/src/packaging/packagingSchema.ts b/ts/packages/agents/onboarding/src/packaging/packagingSchema.ts new file mode 100644 index 0000000000..eb8d1d3618 --- /dev/null +++ b/ts/packages/agents/onboarding/src/packaging/packagingSchema.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Phase 7 — Packaging: build and validate the scaffolded agent package, +// then register it with the TypeAgent dispatcher for end-user testing. + +export type PackagingActions = + | PackageAgentAction + | ValidatePackageAction; + +export type PackageAgentAction = { + actionName: "packageAgent"; + parameters: { + // Integration name to package + integrationName: string; + // If true, also register the agent with the local TypeAgent dispatcher config + register?: boolean; + }; +}; + +export type ValidatePackageAction = { + actionName: "validatePackage"; + parameters: { + // Integration name whose package to validate + integrationName: string; + }; +}; diff --git a/ts/packages/agents/onboarding/src/phraseGen/phraseGenHandler.ts b/ts/packages/agents/onboarding/src/phraseGen/phraseGenHandler.ts new file mode 100644 index 0000000000..7f7a452faf --- /dev/null +++ b/ts/packages/agents/onboarding/src/phraseGen/phraseGenHandler.ts @@ -0,0 +1,277 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Phase 2 — Phrase Generation handler. +// Generates natural language sample phrases for each discovered action +// using an LLM, saved to ~/.typeagent/onboarding//phraseGen/phrases.json + +import { + ActionContext, + TypeAgentAction, + ActionResult, +} from "@typeagent/agent-sdk"; +import { + createActionResultFromTextDisplay, + createActionResultFromMarkdownDisplay, +} from "@typeagent/agent-sdk/helpers/action"; +import { PhraseGenActions } from "./phraseGenSchema.js"; +import { + loadState, + updatePhase, + writeArtifactJson, + readArtifactJson, +} from "../lib/workspace.js"; +import { getPhraseGenModel } from "../lib/llm.js"; +import { ApiSurface, DiscoveredAction } from "../discovery/discoveryHandler.js"; + +export type PhraseSet = { + integrationName: string; + generatedAt: string; + // Map from actionName to array of sample phrases + phrases: Record; + approved?: boolean; + approvedAt?: string; +}; + +export async function executePhraseGenAction( + action: TypeAgentAction, + _context: ActionContext, +): Promise { + switch (action.actionName) { + case "generatePhrases": + return handleGeneratePhrases( + action.parameters.integrationName, + action.parameters.phrasesPerAction ?? 5, + action.parameters.forActions, + ); + + case "addPhrase": + return handleAddPhrase( + action.parameters.integrationName, + action.parameters.actionName, + action.parameters.phrase, + ); + + case "removePhrase": + return handleRemovePhrase( + action.parameters.integrationName, + action.parameters.actionName, + action.parameters.phrase, + ); + + case "approvePhrases": + return handleApprovePhrases(action.parameters.integrationName); + } +} + +async function handleGeneratePhrases( + integrationName: string, + phrasesPerAction: number, + forActions?: string[], +): Promise { + const state = await loadState(integrationName); + if (!state) { + return { error: `Integration "${integrationName}" not found.` }; + } + if (state.phases.discovery.status !== "approved") { + return { error: `Discovery phase must be approved before generating phrases. Run approveApiSurface first.` }; + } + + const surface = await readArtifactJson( + integrationName, + "discovery", + "api-surface.json", + ); + if (!surface) { + return { error: `No API surface found for "${integrationName}".` }; + } + + await updatePhase(integrationName, "phraseGen", { status: "in-progress" }); + + const model = getPhraseGenModel(); + const existing = await readArtifactJson( + integrationName, + "phraseGen", + "phrases.json", + ); + const phraseMap: Record = existing?.phrases ?? {}; + + const actionsToProcess = forActions + ? surface.actions.filter((a) => forActions.includes(a.name)) + : surface.actions; + + for (const discoveredAction of actionsToProcess) { + const prompt = buildPhrasePrompt( + integrationName, + discoveredAction, + phrasesPerAction, + state.config.description, + ); + const result = await model.complete(prompt); + if (!result.success) continue; + + const phrases = extractPhraseList(result.data); + phraseMap[discoveredAction.name] = [ + ...(phraseMap[discoveredAction.name] ?? []), + ...phrases, + ]; + } + + const phraseSet: PhraseSet = { + integrationName, + generatedAt: new Date().toISOString(), + phrases: phraseMap, + }; + + await writeArtifactJson(integrationName, "phraseGen", "phrases.json", phraseSet); + + const totalPhrases = Object.values(phraseMap).reduce( + (sum, p) => sum + p.length, + 0, + ); + + return createActionResultFromMarkdownDisplay( + `## Phrases generated: ${integrationName}\n\n` + + `**Actions covered:** ${Object.keys(phraseMap).length}\n` + + `**Total phrases:** ${totalPhrases}\n\n` + + Object.entries(phraseMap) + .slice(0, 10) + .map( + ([name, phrases]) => + `**${name}:**\n` + + phrases.map((p) => ` - "${p}"`).join("\n"), + ) + .join("\n\n") + + (Object.keys(phraseMap).length > 10 + ? `\n\n_...and ${Object.keys(phraseMap).length - 10} more actions_` + : "") + + `\n\nReview, add/remove phrases as needed, then \`approvePhrases\` to proceed.`, + ); +} + +async function handleAddPhrase( + integrationName: string, + actionName: string, + phrase: string, +): Promise { + const existing = await readArtifactJson( + integrationName, + "phraseGen", + "phrases.json", + ); + const phraseMap = existing?.phrases ?? {}; + if (!phraseMap[actionName]) phraseMap[actionName] = []; + if (!phraseMap[actionName].includes(phrase)) { + phraseMap[actionName].push(phrase); + } + + await writeArtifactJson(integrationName, "phraseGen", "phrases.json", { + ...(existing ?? { integrationName, generatedAt: new Date().toISOString() }), + phrases: phraseMap, + }); + + return createActionResultFromTextDisplay( + `Added phrase "${phrase}" to action "${actionName}" for ${integrationName}.`, + ); +} + +async function handleRemovePhrase( + integrationName: string, + actionName: string, + phrase: string, +): Promise { + const existing = await readArtifactJson( + integrationName, + "phraseGen", + "phrases.json", + ); + if (!existing) { + return { error: `No phrases found for "${integrationName}".` }; + } + + const phrases = existing.phrases[actionName] ?? []; + existing.phrases[actionName] = phrases.filter((p) => p !== phrase); + await writeArtifactJson(integrationName, "phraseGen", "phrases.json", existing); + + return createActionResultFromTextDisplay( + `Removed phrase "${phrase}" from action "${actionName}" for ${integrationName}.`, + ); +} + +async function handleApprovePhrases( + integrationName: string, +): Promise { + const phraseSet = await readArtifactJson( + integrationName, + "phraseGen", + "phrases.json", + ); + if (!phraseSet) { + return { error: `No phrases found for "${integrationName}". Run generatePhrases first.` }; + } + + const updated: PhraseSet = { + ...phraseSet, + approved: true, + approvedAt: new Date().toISOString(), + }; + + await writeArtifactJson(integrationName, "phraseGen", "phrases.json", updated); + await updatePhase(integrationName, "phraseGen", { status: "approved" }); + + const totalPhrases = Object.values(phraseSet.phrases).reduce( + (sum, p) => sum + p.length, + 0, + ); + + return createActionResultFromMarkdownDisplay( + `## Phrases approved: ${integrationName}\n\n` + + `**Actions:** ${Object.keys(phraseSet.phrases).length}\n` + + `**Total phrases:** ${totalPhrases}\n\n` + + `**Next step:** Phase 3 — use \`generateSchema\` to produce the TypeScript action schema.`, + ); +} + +function buildPhrasePrompt( + integrationName: string, + action: DiscoveredAction, + count: number, + appDescription?: string, +): { role: "system" | "user"; content: string }[] { + return [ + { + role: "system", + content: + "You are a UX writer generating natural language phrases that users would say to an AI assistant to perform an API action. " + + "Produce varied, conversational phrases — include different phrasings, politeness levels, and levels of specificity. " + + "Return a JSON array of strings.", + }, + { + role: "user", + content: + `Generate ${count} distinct natural language phrases a user would say to perform this action in ${integrationName}` + + (appDescription ? ` (${appDescription})` : "") + + `.\n\n` + + `Action: ${action.name}\n` + + `Description: ${action.description}\n` + + (action.parameters?.length + ? `Parameters: ${action.parameters.map((p) => `${p.name} (${p.type})`).join(", ")}` + : "") + + `\n\nReturn only a JSON array of strings.`, + }, + ]; +} + +function extractPhraseList(llmResponse: string): string[] { + try { + const jsonMatch = llmResponse.match(/\[[\s\S]*\]/); + if (jsonMatch) { + const parsed = JSON.parse(jsonMatch[0]); + if (Array.isArray(parsed)) { + return parsed.filter((p) => typeof p === "string"); + } + } + } catch {} + // Fallback: extract quoted strings + return [...llmResponse.matchAll(/"([^"]+)"/g)].map((m) => m[1]); +} diff --git a/ts/packages/agents/onboarding/src/phraseGen/phraseGenSchema.agr b/ts/packages/agents/onboarding/src/phraseGen/phraseGenSchema.agr new file mode 100644 index 0000000000..cc3fd1cd12 --- /dev/null +++ b/ts/packages/agents/onboarding/src/phraseGen/phraseGenSchema.agr @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Grammar for Phase 2 — Phrase Generation actions. + +// generatePhrases - generate natural language phrases for all or specific actions + = generate phrases for $(integrationName:wildcard) -> { + actionName: "generatePhrases", + parameters: { + integrationName + } +} + | (create | produce | write) (sample | example | natural language)? phrases for $(integrationName:wildcard) -> { + actionName: "generatePhrases", + parameters: { + integrationName + } +} + | generate $(phrasesPerAction:word) phrases (per action)? for $(integrationName:wildcard) -> { + actionName: "generatePhrases", + parameters: { + integrationName, + phrasesPerAction: phrasesPerAction + } +}; + +// addPhrase - manually add a phrase for a specific action + = add phrase $(phrase:wildcard) for (action)? $(actionName:wildcard) in $(integrationName:wildcard) -> { + actionName: "addPhrase", + parameters: { + phrase, + actionName, + integrationName + } +}; + +// removePhrase - remove a phrase from an action + = remove phrase $(phrase:wildcard) from (action)? $(actionName:wildcard) in $(integrationName:wildcard) -> { + actionName: "removePhrase", + parameters: { + phrase, + actionName, + integrationName + } +}; + +// approvePhrases - lock in the phrase set + = approve phrases for $(integrationName:wildcard) -> { + actionName: "approvePhrases", + parameters: { + integrationName + } +} + | (confirm | finalize) (the)? $(integrationName:wildcard) phrases -> { + actionName: "approvePhrases", + parameters: { + integrationName + } +}; + +import { PhraseGenActions } from "./phraseGenSchema.ts"; + + : PhraseGenActions = + | + | + | ; diff --git a/ts/packages/agents/onboarding/src/phraseGen/phraseGenSchema.ts b/ts/packages/agents/onboarding/src/phraseGen/phraseGenSchema.ts new file mode 100644 index 0000000000..2bcf488e31 --- /dev/null +++ b/ts/packages/agents/onboarding/src/phraseGen/phraseGenSchema.ts @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Phase 2 — Phrase Generation: produce natural language sample phrases +// for each discovered action. Phrases are used to train and validate +// the grammar and schema in later phases. + +export type PhraseGenActions = + | GeneratePhrasesAction + | AddPhraseAction + | RemovePhraseAction + | ApprovePhrasesAction; + +export type GeneratePhrasesAction = { + actionName: "generatePhrases"; + parameters: { + // Integration name to generate phrases for + integrationName: string; + // Number of phrases to generate per action (default: 5) + phrasesPerAction?: number; + // Generate phrases only for these specific action names (generates for all if omitted) + forActions?: string[]; + }; +}; + +export type AddPhraseAction = { + actionName: "addPhrase"; + parameters: { + // Integration name + integrationName: string; + // The action name this phrase should map to + actionName: string; + // The natural language phrase to add + phrase: string; + }; +}; + +export type RemovePhraseAction = { + actionName: "removePhrase"; + parameters: { + // Integration name + integrationName: string; + // The action name to remove the phrase from + actionName: string; + // The exact phrase to remove + phrase: string; + }; +}; + +export type ApprovePhrasesAction = { + actionName: "approvePhrases"; + parameters: { + // Integration name to approve phrases for + integrationName: string; + }; +}; diff --git a/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts b/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts new file mode 100644 index 0000000000..c81620db70 --- /dev/null +++ b/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts @@ -0,0 +1,516 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Phase 5 — Scaffolder handler. +// Stamps out a complete TypeAgent agent package from approved artifacts. +// Templates cover manifest, handler, schema, grammar, package.json, tsconfigs. + +import { + ActionContext, + TypeAgentAction, + ActionResult, +} from "@typeagent/agent-sdk"; +import { + createActionResultFromTextDisplay, + createActionResultFromMarkdownDisplay, +} from "@typeagent/agent-sdk/helpers/action"; +import { ScaffolderActions } from "./scaffolderSchema.js"; +import { + loadState, + updatePhase, + writeArtifact, + readArtifact, + getWorkspacePath, +} from "../lib/workspace.js"; +import fs from "fs/promises"; +import path from "path"; +import os from "os"; + +// Default output root within the TypeAgent repo +const AGENTS_DIR = path.resolve( + new URL(import.meta.url).pathname, + "../../../../../../packages/agents", +); + +export async function executeScaffolderAction( + action: TypeAgentAction, + _context: ActionContext, +): Promise { + switch (action.actionName) { + case "scaffoldAgent": + return handleScaffoldAgent( + action.parameters.integrationName, + action.parameters.outputDir, + ); + case "scaffoldPlugin": + return handleScaffoldPlugin( + action.parameters.integrationName, + action.parameters.template, + action.parameters.outputDir, + ); + case "listTemplates": + return handleListTemplates(); + } +} + +async function handleScaffoldAgent( + integrationName: string, + outputDir?: string, +): Promise { + const state = await loadState(integrationName); + if (!state) return { error: `Integration "${integrationName}" not found.` }; + if (state.phases.grammarGen.status !== "approved") { + return { error: `Grammar phase must be approved first. Run approveGrammar.` }; + } + + const schemaTs = await readArtifact(integrationName, "schemaGen", "schema.ts"); + const grammarAgr = await readArtifact(integrationName, "grammarGen", "schema.agr"); + if (!schemaTs || !grammarAgr) { + return { error: `Missing schema or grammar artifacts for "${integrationName}".` }; + } + + await updatePhase(integrationName, "scaffolder", { status: "in-progress" }); + + // Determine package name and Pascal-case type name + const packageName = `${integrationName}-agent`; + const pascalName = toPascalCase(integrationName); + const targetDir = outputDir ?? path.join(AGENTS_DIR, integrationName); + const srcDir = path.join(targetDir, "src"); + + await fs.mkdir(srcDir, { recursive: true }); + + // Write schema and grammar + await writeFile( + path.join(srcDir, `${integrationName}Schema.ts`), + schemaTs, + ); + await writeFile( + path.join(srcDir, `${integrationName}Schema.agr`), + grammarAgr.replace( + /from "\.\/schema\.ts"/g, + `from "./${integrationName}Schema.ts"`, + ), + ); + + // Stamp out manifest + await writeFile( + path.join(srcDir, `${integrationName}Manifest.json`), + JSON.stringify( + buildManifest(integrationName, pascalName, state.config.description ?? ""), + null, + 2, + ), + ); + + // Stamp out handler + await writeFile( + path.join(srcDir, `${integrationName}ActionHandler.ts`), + buildHandler(integrationName, pascalName), + ); + + // Stamp out package.json + await writeFile( + path.join(targetDir, "package.json"), + JSON.stringify(buildPackageJson(integrationName, packageName, pascalName), null, 2), + ); + + // Stamp out tsconfigs + await writeFile( + path.join(targetDir, "tsconfig.json"), + JSON.stringify(ROOT_TSCONFIG, null, 2), + ); + await writeFile( + path.join(srcDir, "tsconfig.json"), + JSON.stringify(SRC_TSCONFIG, null, 2), + ); + + // Also copy to workspace scaffolder dir for reference + await writeArtifact( + integrationName, + "scaffolder", + "scaffolded-to.txt", + targetDir, + ); + + await updatePhase(integrationName, "scaffolder", { status: "approved" }); + + const files = [ + `src/${integrationName}Schema.ts`, + `src/${integrationName}Schema.agr`, + `src/${integrationName}Manifest.json`, + `src/${integrationName}ActionHandler.ts`, + `package.json`, + `tsconfig.json`, + `src/tsconfig.json`, + ]; + + return createActionResultFromMarkdownDisplay( + `## Agent scaffolded: ${integrationName}\n\n` + + `**Output directory:** \`${targetDir}\`\n\n` + + `**Files created:**\n` + + files.map((f) => `- \`${f}\``).join("\n") + + `\n\n**Next step:** Phase 6 — use \`generateTests\` and \`runTests\` to validate.`, + ); +} + +async function handleScaffoldPlugin( + integrationName: string, + template: string, + outputDir?: string, +): Promise { + const state = await loadState(integrationName); + if (!state) return { error: `Integration "${integrationName}" not found.` }; + + const templateInfo = PLUGIN_TEMPLATES[template]; + if (!templateInfo) { + return { error: `Unknown template "${template}". Use listTemplates to see available templates.` }; + } + + const targetDir = + outputDir ?? + path.join(AGENTS_DIR, integrationName, templateInfo.defaultSubdir); + await fs.mkdir(targetDir, { recursive: true }); + + for (const [filename, content] of Object.entries( + templateInfo.files(integrationName), + )) { + await writeFile(path.join(targetDir, filename), content); + } + + return createActionResultFromMarkdownDisplay( + `## Plugin scaffolded: ${integrationName} (${template})\n\n` + + `**Output:** \`${targetDir}\`\n\n` + + `**Files created:**\n` + + Object.keys(templateInfo.files(integrationName)) + .map((f) => `- \`${f}\``) + .join("\n") + + `\n\n${templateInfo.nextSteps}`, + ); +} + +async function handleListTemplates(): Promise { + const lines = [ + `## Available scaffolding templates`, + ``, + `### Agent templates`, + `- **default** — TypeAgent agent package (manifest, handler, schema, grammar)`, + ``, + `### Plugin templates (use with \`scaffoldPlugin\`)`, + ...Object.entries(PLUGIN_TEMPLATES).map( + ([key, t]) => `- **${key}** — ${t.description}`, + ), + ]; + return createActionResultFromMarkdownDisplay(lines.join("\n")); +} + +// ─── Template helpers ──────────────────────────────────────────────────────── + +function toPascalCase(str: string): string { + return str + .split(/[-_\s]+/) + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(""); +} + +function buildManifest(name: string, pascalName: string, description: string) { + return { + emojiChar: "🔌", + description: description || `Agent for ${name}`, + defaultEnabled: false, + schema: { + description: `${pascalName} agent actions`, + originalSchemaFile: `./${name}Schema.ts`, + schemaFile: `../dist/${name}Schema.pas.json`, + grammarFile: `../dist/${name}Schema.ag.json`, + schemaType: `${pascalName}Actions`, + }, + }; +} + +function buildHandler(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.\`, + ); +} +`; +} + +function buildPackageJson(name: string, packageName: string, pascalName: string) { + return { + name: packageName, + version: "0.0.1", + private: true, + description: `TypeAgent agent for ${name}`, + license: "MIT", + author: "Microsoft", + type: "module", + exports: { + "./agent/manifest": `./src/${name}Manifest.json`, + "./agent/handlers": `./dist/${name}ActionHandler.js`, + }, + scripts: { + asc: `asc -i ./src/${name}Schema.ts -o ./dist/${name}Schema.pas.json -t ${pascalName}Actions`, + agc: `agc -i ./src/${name}Schema.agr -o ./dist/${name}Schema.ag.json`, + build: "concurrently npm:tsc npm:asc npm:agc", + clean: "rimraf --glob dist *.tsbuildinfo *.done.build.log", + tsc: "tsc -b", + }, + dependencies: { + "@typeagent/agent-sdk": "workspace:*", + }, + devDependencies: { + "@typeagent/action-schema-compiler": "workspace:*", + "action-grammar-compiler": "workspace:*", + concurrently: "^9.1.2", + rimraf: "^6.0.1", + typescript: "~5.4.5", + }, + }; +} + +const ROOT_TSCONFIG = { + extends: "../../../tsconfig.base.json", + compilerOptions: { composite: true }, + include: [], + references: [{ path: "./src" }], + "ts-node": { esm: true }, +}; + +const SRC_TSCONFIG = { + extends: "../../../../tsconfig.base.json", + compilerOptions: { composite: true, rootDir: ".", outDir: "../dist" }, + include: ["./**/*"], + "ts-node": { esm: true }, +}; + +const PLUGIN_TEMPLATES: Record< + string, + { + description: string; + defaultSubdir: string; + nextSteps: string; + files: (name: string) => Record; + } +> = { + "rest-client": { + description: "Simple REST API client bridge", + defaultSubdir: "src", + nextSteps: + "Implement `executeCommand(action, params)` to call your REST API endpoints.", + files: (name) => ({ + [`${name}Bridge.ts`]: buildRestClientTemplate(name), + }), + }, + "websocket-bridge": { + description: + "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.", + files: (name) => ({ + [`${name}Bridge.ts`]: buildWebSocketBridgeTemplate(name), + }), + }, + "office-addin": { + description: "Office.js task pane add-in skeleton", + defaultSubdir: "addin", + nextSteps: + "Load the add-in in Excel/Word/Outlook and configure the manifest URL.", + files: (name) => ({ + "taskpane.html": buildOfficeAddinHtml(name), + "taskpane.ts": buildOfficeAddinTs(name), + "manifest.xml": buildOfficeManifestXml(name), + }), + }, +}; + +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; + } +} +`; +} + +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)); + }); + } +} +`; +} + +function buildOfficeAddinHtml(name: string): string { + return ` + + + + ${toPascalCase(name)} TypeAgent Add-in + + + + +

${toPascalCase(name)} TypeAgent

+
Connecting...
+ + +`; +} + +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}\`); +} +`; +} + +function buildOfficeManifestXml(name: string): string { + const pascal = toPascalCase(name); + return ` + + + 1.0.0.0 + Microsoft + en-US + + + + + + + + + ReadWriteDocument + +`; +} + +async function writeFile(filePath: string, content: string): Promise { + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, content, "utf-8"); +} diff --git a/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.agr b/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.agr new file mode 100644 index 0000000000..e50d9c3b16 --- /dev/null +++ b/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.agr @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Grammar for Phase 5 — Scaffolder actions. + + = scaffold (the)? $(integrationName:wildcard) agent -> { + actionName: "scaffoldAgent", + parameters: { + integrationName + } +} + | (create | generate | stamp out) (the)? $(integrationName:wildcard) (agent)? (package)? -> { + actionName: "scaffoldAgent", + parameters: { + integrationName + } +}; + + = scaffold (the)? $(integrationName:wildcard) (plugin | add-in | extension) -> { + actionName: "scaffoldPlugin", + parameters: { + integrationName, + template: "rest-client" + } +} + | (create | generate) (the)? $(integrationName:wildcard) $(template:word) (plugin | extension | add-in) -> { + actionName: "scaffoldPlugin", + parameters: { + integrationName, + template + } +}; + + = list (available)? templates -> { + actionName: "listTemplates", + parameters: {} +} + | (show | what are) (the)? (available)? (scaffolding)? templates -> { + actionName: "listTemplates", + parameters: {} +}; + +import { ScaffolderActions } from "./scaffolderSchema.ts"; + + : ScaffolderActions = + | + | ; diff --git a/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.ts b/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.ts new file mode 100644 index 0000000000..f022977090 --- /dev/null +++ b/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.ts @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Phase 5 — Scaffolder: stamp out the complete TypeAgent agent package +// from the approved schema, grammar, and config. Output goes to +// ~/.typeagent/onboarding//scaffolder/agent/ + +export type ScaffolderActions = + | ScaffoldAgentAction + | ScaffoldPluginAction + | ListTemplatesAction; + +export type ScaffoldAgentAction = { + actionName: "scaffoldAgent"; + parameters: { + // Integration name to scaffold agent for + integrationName: string; + // Target directory for the agent package (defaults to ts/packages/agents/) + outputDir?: string; + }; +}; + +export type ScaffoldPluginAction = { + actionName: "scaffoldPlugin"; + parameters: { + // Integration name to scaffold the host-side plugin for + integrationName: string; + // Template to use for the plugin side + template: "office-addin" | "vscode-extension" | "electron-app" | "browser-extension" | "rest-client"; + // Target directory for the plugin (defaults to ts/packages/agents//plugin) + outputDir?: string; + }; +}; + +export type ListTemplatesAction = { + actionName: "listTemplates"; + parameters: Record; +}; diff --git a/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts b/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts new file mode 100644 index 0000000000..98625b1cec --- /dev/null +++ b/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts @@ -0,0 +1,193 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Phase 3 — Schema Generation handler. +// Uses the approved API surface and generated phrases to produce a +// TypeScript action schema file with appropriate comments. + +import { + ActionContext, + TypeAgentAction, + ActionResult, +} from "@typeagent/agent-sdk"; +import { + createActionResultFromTextDisplay, + createActionResultFromMarkdownDisplay, +} from "@typeagent/agent-sdk/helpers/action"; +import { SchemaGenActions } from "./schemaGenSchema.js"; +import { + loadState, + updatePhase, + writeArtifact, + readArtifact, + readArtifactJson, +} from "../lib/workspace.js"; +import { getSchemaGenModel } from "../lib/llm.js"; +import { ApiSurface } from "../discovery/discoveryHandler.js"; +import { PhraseSet } from "../phraseGen/phraseGenHandler.js"; + +export async function executeSchemaGenAction( + action: TypeAgentAction, + _context: ActionContext, +): Promise { + switch (action.actionName) { + case "generateSchema": + return handleGenerateSchema(action.parameters.integrationName); + + case "refineSchema": + return handleRefineSchema( + action.parameters.integrationName, + action.parameters.instructions, + ); + + case "approveSchema": + return handleApproveSchema(action.parameters.integrationName); + } +} + +async function handleGenerateSchema(integrationName: string): Promise { + const state = await loadState(integrationName); + if (!state) return { error: `Integration "${integrationName}" not found.` }; + if (state.phases.phraseGen.status !== "approved") { + return { error: `Phrase generation phase must be approved first. Run approvePhrases.` }; + } + + const surface = await readArtifactJson(integrationName, "discovery", "api-surface.json"); + const phraseSet = await readArtifactJson(integrationName, "phraseGen", "phrases.json"); + if (!surface || !phraseSet) { + return { error: `Missing discovery or phrase artifacts for "${integrationName}".` }; + } + + await updatePhase(integrationName, "schemaGen", { status: "in-progress" }); + + const model = getSchemaGenModel(); + const prompt = buildSchemaPrompt(integrationName, surface, phraseSet, state.config.description); + const result = await model.complete(prompt); + if (!result.success) { + return { error: `Schema generation failed: ${result.message}` }; + } + + const schemaTs = extractTypeScript(result.data); + await writeArtifact(integrationName, "schemaGen", "schema.ts", schemaTs); + + return createActionResultFromMarkdownDisplay( + `## Schema generated: ${integrationName}\n\n` + + "```typescript\n" + schemaTs.slice(0, 2000) + (schemaTs.length > 2000 ? "\n// ... (truncated)" : "") + "\n```\n\n" + + `Use \`refineSchema\` to adjust, or \`approveSchema\` to proceed to grammar generation.`, + ); +} + +async function handleRefineSchema( + integrationName: string, + instructions: string, +): Promise { + const existing = await readArtifact(integrationName, "schemaGen", "schema.ts"); + if (!existing) { + return { error: `No schema found for "${integrationName}". Run generateSchema first.` }; + } + + const model = getSchemaGenModel(); + const prompt = [ + { + role: "system" as const, + content: + "You are a TypeScript expert. Modify the given TypeAgent action schema according to the instructions. " + + "Preserve all copyright headers and existing structure. Return only the updated TypeScript file content.", + }, + { + role: "user" as const, + content: + `Refine this TypeAgent action schema for "${integrationName}".\n\n` + + `Instructions: ${instructions}\n\n` + + `Current schema:\n\`\`\`typescript\n${existing}\n\`\`\``, + }, + ]; + + const result = await model.complete(prompt); + if (!result.success) { + return { error: `Schema refinement failed: ${result.message}` }; + } + + const refined = extractTypeScript(result.data); + // Archive the previous version + const version = Date.now(); + await writeArtifact(integrationName, "schemaGen", `schema.v${version}.ts`, existing); + await writeArtifact(integrationName, "schemaGen", "schema.ts", refined); + + return createActionResultFromMarkdownDisplay( + `## Schema refined: ${integrationName}\n\n` + + `Previous version archived as \`schema.v${version}.ts\`\n\n` + + "```typescript\n" + refined.slice(0, 2000) + (refined.length > 2000 ? "\n// ... (truncated)" : "") + "\n```", + ); +} + +async function handleApproveSchema(integrationName: string): Promise { + const schema = await readArtifact(integrationName, "schemaGen", "schema.ts"); + if (!schema) { + return { error: `No schema found for "${integrationName}". Run generateSchema first.` }; + } + + await updatePhase(integrationName, "schemaGen", { status: "approved" }); + + return createActionResultFromMarkdownDisplay( + `## Schema approved: ${integrationName}\n\n` + + `Schema saved to \`~/.typeagent/onboarding/${integrationName}/schemaGen/schema.ts\`\n\n` + + `**Next step:** Phase 4 — use \`generateGrammar\` to produce the .agr grammar file.`, + ); +} + +function buildSchemaPrompt( + integrationName: string, + surface: ApiSurface, + phraseSet: PhraseSet, + description?: string, +): { role: "system" | "user"; content: string }[] { + const actionSummary = surface.actions + .map((a) => { + const phrases = phraseSet.phrases[a.name] ?? []; + return ( + `Action: ${a.name}\n` + + `Description: ${a.description}\n` + + (a.parameters?.length + ? `Parameters: ${a.parameters.map((p) => `${p.name}: ${p.type}${p.required ? "" : "?"}`).join(", ")}\n` + : "") + + (phrases.length + ? `Sample phrases:\n${phrases.slice(0, 3).map((p) => ` - "${p}"`).join("\n")}` + : "") + ); + }) + .join("\n\n"); + + return [ + { + role: "system", + content: + "You are a TypeScript expert generating TypeAgent action schemas. " + + "TypeAgent action schemas are TypeScript union types where each member has an `actionName` discriminant and a `parameters` object. " + + "Add JSDoc comments to each parameter explaining its purpose and valid values. " + + "Follow these conventions:\n" + + "- Export a top-level union type named `Actions`\n" + + "- Each action type is named `Action`\n" + + "- Use `actionName: \"camelCaseName\"` as a string literal type\n" + + "- Parameters use camelCase names\n" + + "- Optional parameters use `?: type` syntax\n" + + "- Include the copyright header\n" + + "Return only the TypeScript file content.", + }, + { + role: "user", + content: + `Generate a TypeAgent action schema for the "${integrationName}" integration` + + (description ? ` (${description})` : "") + + `.\n\n` + + `Actions to include:\n\n${actionSummary}`, + }, + ]; +} + +function extractTypeScript(llmResponse: string): string { + // Strip markdown code fences if present + const fenceMatch = llmResponse.match(/```(?:typescript|ts)?\n([\s\S]*?)```/); + if (fenceMatch) return fenceMatch[1].trim(); + return llmResponse.trim(); +} diff --git a/ts/packages/agents/onboarding/src/schemaGen/schemaGenSchema.agr b/ts/packages/agents/onboarding/src/schemaGen/schemaGenSchema.agr new file mode 100644 index 0000000000..e5987f97c5 --- /dev/null +++ b/ts/packages/agents/onboarding/src/schemaGen/schemaGenSchema.agr @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Grammar for Phase 3 — Schema Generation actions. + +// generateSchema - produce TypeScript action schema from discovered actions + phrases + = generate (the)? (action)? schema for $(integrationName:wildcard) -> { + actionName: "generateSchema", + parameters: { + integrationName + } +} + | (create | produce | write) (the)? (typescript)? (action)? schema for $(integrationName:wildcard) -> { + actionName: "generateSchema", + parameters: { + integrationName + } +}; + +// refineSchema - update the schema based on instructions + = refine (the)? $(integrationName:wildcard) schema (to)? $(instructions:wildcard) -> { + actionName: "refineSchema", + parameters: { + integrationName, + instructions + } +} + | update (the)? $(integrationName:wildcard) schema (to)? $(instructions:wildcard) -> { + actionName: "refineSchema", + parameters: { + integrationName, + instructions + } +}; + +// approveSchema - lock in the schema + = approve (the)? $(integrationName:wildcard) schema -> { + actionName: "approveSchema", + parameters: { + integrationName + } +} + | (confirm | finalize) (the)? $(integrationName:wildcard) (action)? schema -> { + actionName: "approveSchema", + parameters: { + integrationName + } +}; + +import { SchemaGenActions } from "./schemaGenSchema.ts"; + + : SchemaGenActions = + | + | ; diff --git a/ts/packages/agents/onboarding/src/schemaGen/schemaGenSchema.ts b/ts/packages/agents/onboarding/src/schemaGen/schemaGenSchema.ts new file mode 100644 index 0000000000..bb062892e4 --- /dev/null +++ b/ts/packages/agents/onboarding/src/schemaGen/schemaGenSchema.ts @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Phase 3 — Schema Generation: produce a TypeScript action schema file +// that maps natural language user requests to the target API surface. +// Output is saved to ~/.typeagent/onboarding//schemaGen/schema.ts + +export type SchemaGenActions = + | GenerateSchemaAction + | RefineSchemaAction + | ApproveSchemaAction; + +export type GenerateSchemaAction = { + actionName: "generateSchema"; + parameters: { + // Integration name to generate schema for + integrationName: string; + }; +}; + +export type RefineSchemaAction = { + actionName: "refineSchema"; + parameters: { + // Integration name + integrationName: string; + // Specific instructions for the LLM about what to change + // e.g. "make the listName parameter optional" or "add a sortOrder parameter to sortAction" + instructions: string; + }; +}; + +export type ApproveSchemaAction = { + actionName: "approveSchema"; + parameters: { + // Integration name to approve schema for + integrationName: string; + }; +}; diff --git a/ts/packages/agents/onboarding/src/testing/testingHandler.ts b/ts/packages/agents/onboarding/src/testing/testingHandler.ts new file mode 100644 index 0000000000..f3341e4be1 --- /dev/null +++ b/ts/packages/agents/onboarding/src/testing/testingHandler.ts @@ -0,0 +1,545 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Phase 6 — Testing handler. +// Generates phrase→action test cases from the approved phrase set, +// runs them against the dispatcher using createDispatcher (same pattern +// as evalHarness.ts), and uses an LLM to propose repairs for failures. + +import { + ActionContext, + TypeAgentAction, + ActionResult, +} from "@typeagent/agent-sdk"; +import { + createActionResultFromTextDisplay, + createActionResultFromMarkdownDisplay, +} from "@typeagent/agent-sdk/helpers/action"; +import { TestingActions } from "./testingSchema.js"; +import { + loadState, + updatePhase, + writeArtifactJson, + readArtifactJson, + readArtifact, +} from "../lib/workspace.js"; +import { getTestingModel } from "../lib/llm.js"; +import { PhraseSet } from "../phraseGen/phraseGenHandler.js"; +import { createDispatcher } from "agent-dispatcher"; +import { getDefaultAppAgentProviders } from "default-agent-provider"; +import { getFsStorageProvider } from "dispatcher-node-providers"; +import { getInstanceDir } from "agent-dispatcher/helpers/data"; +import type { + ClientIO, + IAgentMessage, + RequestId, + CommandResult, +} from "@typeagent/dispatcher-types"; +import type { DisplayAppendMode, DisplayContent, MessageContent } from "@typeagent/agent-sdk"; + +export type TestCase = { + phrase: string; + expectedActionName: string; + // Expected parameter values (partial match is acceptable) + expectedParameters?: Record; +}; + +export type TestResult = { + phrase: string; + expectedActionName: string; + actualActionName?: string; + passed: boolean; + error?: string; +}; + +export type TestRun = { + integrationName: string; + ranAt: string; + total: number; + passed: number; + failed: number; + results: TestResult[]; +}; + +export type ProposedRepair = { + integrationName: string; + proposedAt: string; + // Suggested changes to the schema file + schemaChanges?: string; + // Suggested changes to the grammar file + grammarChanges?: string; + // Explanation of what was wrong and why these changes fix it + rationale: string; + applied?: boolean; + appliedAt?: string; +}; + +export async function executeTestingAction( + action: TypeAgentAction, + context: ActionContext, +): Promise { + switch (action.actionName) { + case "generateTests": + return handleGenerateTests(action.parameters.integrationName); + + case "runTests": + return handleRunTests( + action.parameters.integrationName, + context, // passed through for future session context use + action.parameters.forActions, + action.parameters.limit, + ); + + case "getTestResults": + return handleGetTestResults( + action.parameters.integrationName, + action.parameters.filter, + ); + + case "proposeRepair": + return handleProposeRepair( + action.parameters.integrationName, + action.parameters.forActions, + ); + + case "approveRepair": + return handleApproveRepair(action.parameters.integrationName); + } +} + +async function handleGenerateTests(integrationName: string): Promise { + const state = await loadState(integrationName); + if (!state) return { error: `Integration "${integrationName}" not found.` }; + if (state.phases.scaffolder.status !== "approved") { + return { error: `Scaffolder phase must be approved first. Run scaffoldAgent.` }; + } + + const phraseSet = await readArtifactJson( + integrationName, + "phraseGen", + "phrases.json", + ); + if (!phraseSet) { + return { error: `No phrases found for "${integrationName}".` }; + } + + await updatePhase(integrationName, "testing", { status: "in-progress" }); + + // Convert phrase set to test cases + const testCases: TestCase[] = []; + for (const [actionName, phrases] of Object.entries(phraseSet.phrases)) { + for (const phrase of phrases) { + testCases.push({ + phrase, + expectedActionName: actionName, + }); + } + } + + await writeArtifactJson( + integrationName, + "testing", + "test-cases.json", + testCases, + ); + + return createActionResultFromMarkdownDisplay( + `## Test cases generated: ${integrationName}\n\n` + + `**Total test cases:** ${testCases.length}\n` + + `**Actions covered:** ${Object.keys(phraseSet.phrases).length}\n\n` + + `Use \`runTests\` to execute them against the dispatcher.`, + ); +} + +async function handleRunTests( + integrationName: string, + _context: ActionContext, + forActions?: string[], + limit?: number, +): Promise { + const testCases = await readArtifactJson( + integrationName, + "testing", + "test-cases.json", + ); + if (!testCases || testCases.length === 0) { + return { error: `No test cases found for "${integrationName}". Run generateTests first.` }; + } + + let toRun = forActions + ? testCases.filter((tc) => forActions.includes(tc.expectedActionName)) + : testCases; + if (limit) toRun = toRun.slice(0, limit); + + // Create a dispatcher and run each phrase through it. + // The scaffolded agent must be registered in config.json before running tests. + // Use `packageAgent --register` (phase 7) or add manually and restart TypeAgent. + let dispatcherSession: Awaited> | undefined; + try { + dispatcherSession = await createTestDispatcher(); + } catch (err: any) { + return { + error: + `Failed to create dispatcher: ${err?.message ?? err}\n\n` + + `Make sure the "${integrationName}" agent is registered in config.json ` + + `and TypeAgent has been restarted. Run \`packageAgent --register\` first.`, + }; + } + + const results: TestResult[] = []; + for (const tc of toRun) { + const result = await runSingleTest(tc, integrationName, dispatcherSession.dispatcher); + results.push(result); + } + + await dispatcherSession.dispatcher.close(); + + const passed = results.filter((r) => r.passed).length; + const failed = results.length - passed; + + const testRun: TestRun = { + integrationName, + ranAt: new Date().toISOString(), + total: results.length, + passed, + failed, + results, + }; + + await writeArtifactJson(integrationName, "testing", "results.json", testRun); + + const passRate = Math.round((passed / results.length) * 100); + + const failingSummary = results + .filter((r) => !r.passed) + .slice(0, 10) + .map( + (r) => + `- ❌ "${r.phrase}" → expected \`${r.expectedActionName}\`, got \`${r.actualActionName ?? "error"}\`${r.error ? ` (${r.error})` : ""}`, + ) + .join("\n"); + + return createActionResultFromMarkdownDisplay( + `## Test results: ${integrationName}\n\n` + + `**Pass rate:** ${passRate}% (${passed}/${results.length})\n\n` + + (failed > 0 + ? `**Failing tests (first 10):**\n${failingSummary}\n\n` + + `Use \`proposeRepair\` to get LLM-suggested schema/grammar fixes.` + : `All tests passed! Use \`approveRepair\` to finalize or proceed to packaging.`), + ); +} + +async function handleGetTestResults( + integrationName: string, + filter?: "passing" | "failing", +): Promise { + const testRun = await readArtifactJson( + integrationName, + "testing", + "results.json", + ); + if (!testRun) { + return { error: `No test results found for "${integrationName}". Run runTests first.` }; + } + + const results = filter + ? testRun.results.filter((r) => + filter === "passing" ? r.passed : !r.passed, + ) + : testRun.results; + + const lines = [ + `## Test results: ${integrationName}`, + ``, + `**Run at:** ${testRun.ranAt}`, + `**Total:** ${testRun.total} | **Passed:** ${testRun.passed} | **Failed:** ${testRun.failed}`, + ``, + `| Result | Phrase | Expected | Actual |`, + `|---|---|---|---|`, + ...results.slice(0, 50).map( + (r) => + `| ${r.passed ? "✅" : "❌"} | "${r.phrase}" | \`${r.expectedActionName}\` | \`${r.actualActionName ?? r.error ?? "—"}\` |`, + ), + ]; + if (results.length > 50) { + lines.push(``, `_...and ${results.length - 50} more_`); + } + + return createActionResultFromMarkdownDisplay(lines.join("\n")); +} + +async function handleProposeRepair( + integrationName: string, + forActions?: string[], +): Promise { + const testRun = await readArtifactJson( + integrationName, + "testing", + "results.json", + ); + if (!testRun) { + return { error: `No test results found. Run runTests first.` }; + } + + const failing = testRun.results.filter((r) => !r.passed); + if (failing.length === 0) { + return createActionResultFromTextDisplay( + "All tests are passing — no repairs needed.", + ); + } + + const schemaTs = await readArtifact(integrationName, "schemaGen", "schema.ts"); + const grammarAgr = await readArtifact(integrationName, "grammarGen", "schema.agr"); + + const filteredFailing = forActions + ? failing.filter((r) => forActions.includes(r.expectedActionName)) + : failing; + + const model = getTestingModel(); + const prompt = buildRepairPrompt( + integrationName, + filteredFailing, + schemaTs ?? "", + grammarAgr ?? "", + ); + + const result = await model.complete(prompt); + if (!result.success) { + return { error: `Repair proposal failed: ${result.message}` }; + } + + const repair: ProposedRepair = { + integrationName, + proposedAt: new Date().toISOString(), + rationale: result.data, + }; + + // Extract suggested schema and grammar changes from the response + const schemaMatch = result.data.match(/```typescript([\s\S]*?)```/); + const grammarMatch = result.data.match(/```(?:agr)?([\s\S]*?)```/); + if (schemaMatch) repair.schemaChanges = schemaMatch[1].trim(); + if (grammarMatch) repair.grammarChanges = grammarMatch[1].trim(); + + await writeArtifactJson( + integrationName, + "testing", + "proposed-repair.json", + repair, + ); + + return createActionResultFromMarkdownDisplay( + `## Proposed repair: ${integrationName}\n\n` + + `**Failing tests addressed:** ${filteredFailing.length}\n\n` + + result.data.slice(0, 3000) + + (result.data.length > 3000 ? "\n\n_...truncated_" : "") + + `\n\nReview the proposed changes, then use \`approveRepair\` to apply them.`, + ); +} + +async function handleApproveRepair(integrationName: string): Promise { + const repair = await readArtifactJson( + integrationName, + "testing", + "proposed-repair.json", + ); + if (!repair) { + return { error: `No proposed repair found. Run proposeRepair first.` }; + } + if (repair.applied) { + return createActionResultFromTextDisplay("Repair was already applied."); + } + + // Apply schema changes if present + if (repair.schemaChanges) { + const version = Date.now(); + const existing = await readArtifact(integrationName, "schemaGen", "schema.ts"); + if (existing) { + await writeArtifactJson( + integrationName, + "testing", + `schema.backup.v${version}.ts`, + existing, + ); + } + const { writeArtifact } = await import("../lib/workspace.js"); + await writeArtifact(integrationName, "schemaGen", "schema.ts", repair.schemaChanges); + } + + // Apply grammar changes if present + if (repair.grammarChanges) { + const version = Date.now(); + const existing = await readArtifact(integrationName, "grammarGen", "schema.agr"); + if (existing) { + await writeArtifactJson( + integrationName, + "testing", + `grammar.backup.v${version}.agr`, + existing, + ); + } + const { writeArtifact } = await import("../lib/workspace.js"); + await writeArtifact(integrationName, "grammarGen", "schema.agr", repair.grammarChanges); + } + + repair.applied = true; + repair.appliedAt = new Date().toISOString(); + await writeArtifactJson(integrationName, "testing", "proposed-repair.json", repair); + + await updatePhase(integrationName, "testing", { status: "approved" }); + + return createActionResultFromMarkdownDisplay( + `## Repair applied: ${integrationName}\n\n` + + (repair.schemaChanges ? "- Schema updated\n" : "") + + (repair.grammarChanges ? "- Grammar updated\n" : "") + + `\nRe-run \`runTests\` to verify fixes, or \`packageAgent\` to proceed.`, + ); +} + +// ─── Dispatcher helpers ─────────────────────────────────────────────────────── + +// Minimal ClientIO that silently captures display output into a buffer. +// Mirrors the createCapturingClientIO pattern from evalHarness.ts. +function createCapturingClientIO(buffer: string[]): ClientIO { + const noop = (() => {}) as (...args: any[]) => any; + + function contentToText(content: DisplayContent): string { + if (typeof content === "string") return content; + if (Array.isArray(content)) { + if (content.length === 0) return ""; + if (typeof content[0] === "string") return (content as string[]).join("\n"); + return (content as string[][]).map((r) => r.join(" | ")).join("\n"); + } + // TypedDisplayContent + const msg = (content as any).content as MessageContent; + if (typeof msg === "string") return msg; + if (Array.isArray(msg)) return (msg as string[]).join("\n"); + return String(msg); + } + + return { + clear: noop, + exit: () => process.exit(0), + setUserRequest: noop, + setDisplayInfo: noop, + setDisplay(message: IAgentMessage) { + const text = contentToText(message.message); + if (text) buffer.push(text); + }, + appendDisplay(message: IAgentMessage, _mode: DisplayAppendMode) { + const text = contentToText(message.message); + if (text) buffer.push(text); + }, + appendDiagnosticData: noop, + setDynamicDisplay: noop, + askYesNo: async (_id: RequestId, _msg: string, def = false) => def, + proposeAction: async () => undefined, + popupQuestion: async () => { throw new Error("popupQuestion not supported in test runner"); }, + notify: noop, + openLocalView: async () => {}, + closeLocalView: async () => {}, + requestChoice: noop, + takeAction: noop, + } satisfies ClientIO; +} + +// Create a dispatcher wired to the default agent providers. +// The scaffolded agent must be registered in the TypeAgent config before +// running tests (use `packageAgent --register` or add manually to config.json). +async function createTestDispatcher() { + const instanceDir = getInstanceDir(); + const appAgentProviders = getDefaultAppAgentProviders(instanceDir); + const buffer: string[] = []; + const clientIO = createCapturingClientIO(buffer); + + const dispatcher = await createDispatcher("onboarding-test-runner", { + appAgentProviders, + agents: { commands: ["dispatcher"] }, + explainer: { enabled: false }, + cache: { enabled: false }, + collectCommandResult: true, + persistDir: instanceDir, + storageProvider: getFsStorageProvider(), + clientIO, + dblogging: false, + }); + + return { dispatcher, buffer }; +} + +async function runSingleTest( + tc: TestCase, + integrationName: string, + dispatcher: Awaited>["dispatcher"], +): Promise { + // Route to the specific integration agent: "@ " + const command = `@${integrationName} ${tc.phrase}`; + + let result: CommandResult | undefined; + try { + result = await dispatcher.processCommand(command); + } catch (err: any) { + return { + phrase: tc.phrase, + expectedActionName: tc.expectedActionName, + passed: false, + error: err?.message ?? String(err), + }; + } + + if (result?.lastError) { + return { + phrase: tc.phrase, + expectedActionName: tc.expectedActionName, + passed: false, + error: result.lastError, + }; + } + + // Check the first dispatched action's name against expected + const actualActionName = result?.actions?.[0]?.actionName; + const passed = actualActionName === tc.expectedActionName; + + return { + phrase: tc.phrase, + expectedActionName: tc.expectedActionName, + actualActionName, + passed, + error: passed + ? undefined + : `Expected "${tc.expectedActionName}", got "${actualActionName ?? "no action"}"`, + }; +} + +function buildRepairPrompt( + integrationName: string, + failing: TestResult[], + schemaTs: string, + grammarAgr: string, +): { role: "system" | "user"; content: string }[] { + const failuresSummary = failing + .slice(0, 20) + .map( + (r) => + `Phrase: "${r.phrase}"\nExpected: ${r.expectedActionName}\nGot: ${r.actualActionName ?? r.error ?? "no match"}`, + ) + .join("\n\n"); + + return [ + { + role: "system", + content: + "You are a TypeAgent grammar and schema expert. Analyze failing phrase-to-action test cases " + + "and propose specific fixes to the TypeScript schema and/or .agr grammar file. " + + "Explain what is wrong and why your changes will fix it. " + + "Provide the updated TypeScript schema in a ```typescript block and/or the updated grammar in a ```agr block.", + }, + { + role: "user", + content: + `Fix the TypeAgent schema and grammar for "${integrationName}" to make these failing tests pass.\n\n` + + `Failing tests (${failing.length} total, showing first 20):\n\n${failuresSummary}\n\n` + + `Current schema:\n\`\`\`typescript\n${schemaTs.slice(0, 3000)}\n\`\`\`\n\n` + + `Current grammar:\n\`\`\`agr\n${grammarAgr.slice(0, 3000)}\n\`\`\``, + }, + ]; +} diff --git a/ts/packages/agents/onboarding/src/testing/testingSchema.agr b/ts/packages/agents/onboarding/src/testing/testingSchema.agr new file mode 100644 index 0000000000..5c6f4ed2ec --- /dev/null +++ b/ts/packages/agents/onboarding/src/testing/testingSchema.agr @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Grammar for Phase 6 — Testing actions. + + = generate tests for $(integrationName:wildcard) -> { + actionName: "generateTests", + parameters: { + integrationName + } +} + | (create | produce) (test cases | tests) for $(integrationName:wildcard) -> { + actionName: "generateTests", + parameters: { + integrationName + } +}; + + = run tests for $(integrationName:wildcard) -> { + actionName: "runTests", + parameters: { + integrationName + } +} + | (execute | run) (the)? $(integrationName:wildcard) tests -> { + actionName: "runTests", + parameters: { + integrationName + } +}; + + = (get | show | display) (the)? $(integrationName:wildcard) test results -> { + actionName: "getTestResults", + parameters: { + integrationName + } +} + | (what are | show me) (the)? (test)? results for $(integrationName:wildcard) -> { + actionName: "getTestResults", + parameters: { + integrationName + } +}; + + = propose (a)? repair for $(integrationName:wildcard) -> { + actionName: "proposeRepair", + parameters: { + integrationName + } +} + | (suggest | find) (a)? fix for (the)? $(integrationName:wildcard) (test)? failures -> { + actionName: "proposeRepair", + parameters: { + integrationName + } +}; + + = approve (the)? $(integrationName:wildcard) repair -> { + actionName: "approveRepair", + parameters: { + integrationName + } +} + | (apply | accept) (the)? $(integrationName:wildcard) (proposed)? (repair | fix) -> { + actionName: "approveRepair", + parameters: { + integrationName + } +}; + +import { TestingActions } from "./testingSchema.ts"; + + : TestingActions = + | + | + | + | ; diff --git a/ts/packages/agents/onboarding/src/testing/testingSchema.ts b/ts/packages/agents/onboarding/src/testing/testingSchema.ts new file mode 100644 index 0000000000..efd7f89ca3 --- /dev/null +++ b/ts/packages/agents/onboarding/src/testing/testingSchema.ts @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Phase 6 — Testing: generate test cases from sample phrases and run a +// phrase-to-action validation loop, proposing repairs for failures. + +export type TestingActions = + | GenerateTestsAction + | RunTestsAction + | GetTestResultsAction + | ProposeRepairAction + | ApproveRepairAction; + +export type GenerateTestsAction = { + actionName: "generateTests"; + parameters: { + // Integration name to generate tests for + integrationName: string; + }; +}; + +export type RunTestsAction = { + actionName: "runTests"; + parameters: { + // Integration name to run tests for + integrationName: string; + // Run only tests for these specific action names + forActions?: string[]; + // Maximum number of tests to run (runs all if omitted) + limit?: number; + }; +}; + +export type GetTestResultsAction = { + actionName: "getTestResults"; + parameters: { + // Integration name to get test results for + integrationName: string; + // Filter to show only passing or failing tests + filter?: "passing" | "failing"; + }; +}; + +export type ProposeRepairAction = { + actionName: "proposeRepair"; + parameters: { + // Integration name to propose repairs for + integrationName: string; + // If provided, propose repairs only for these specific failing action names + forActions?: string[]; + }; +}; + +export type ApproveRepairAction = { + actionName: "approveRepair"; + parameters: { + // Integration name to approve the proposed repair for + integrationName: string; + }; +}; diff --git a/ts/packages/agents/onboarding/src/tsconfig.json b/ts/packages/agents/onboarding/src/tsconfig.json new file mode 100644 index 0000000000..85efcd566d --- /dev/null +++ b/ts/packages/agents/onboarding/src/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "rootDir": ".", + "outDir": "../dist" + }, + "include": ["./**/*"], + "ts-node": { + "esm": true + } +} diff --git a/ts/packages/agents/onboarding/tsconfig.json b/ts/packages/agents/onboarding/tsconfig.json new file mode 100644 index 0000000000..acb9cb4a91 --- /dev/null +++ b/ts/packages/agents/onboarding/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true + }, + "include": [], + "references": [{ "path": "./src" }], + "ts-node": { + "esm": true + } +} From 5b3bb006f790533dfa3c88d49d6251786b93ae1c Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Fri, 3 Apr 2026 19:29:01 -0700 Subject: [PATCH 02/33] added onboarding agent to the dispatcher config --- ts/packages/defaultAgentProvider/data/config.json | 3 +++ ts/packages/defaultAgentProvider/package.json | 1 + 2 files changed, 4 insertions(+) diff --git a/ts/packages/defaultAgentProvider/data/config.json b/ts/packages/defaultAgentProvider/data/config.json index 5a31b154e3..2d3feff0be 100644 --- a/ts/packages/defaultAgentProvider/data/config.json +++ b/ts/packages/defaultAgentProvider/data/config.json @@ -63,6 +63,9 @@ }, "utility": { "name": "utility-typeagent" + }, + "onboarding": { + "name": "onboarding-agent" } }, "mcpServers": { diff --git a/ts/packages/defaultAgentProvider/package.json b/ts/packages/defaultAgentProvider/package.json index 93c027cbb9..c8a7d331ac 100644 --- a/ts/packages/defaultAgentProvider/package.json +++ b/ts/packages/defaultAgentProvider/package.json @@ -66,6 +66,7 @@ "montage-agent": "workspace:*", "music": "workspace:*", "music-local": "workspace:*", + "onboarding-agent": "workspace:*", "photo-agent": "workspace:*", "proper-lockfile": "^4.1.2", "settings-agent": "workspace:*", From dbb9edabd8ee86fe2e9648d8008e2b3bbfd3f0ad Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Mon, 6 Apr 2026 09:52:40 -0700 Subject: [PATCH 03/33] updated lock file --- ts/pnpm-lock.yaml | 67 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 52 insertions(+), 15 deletions(-) diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml index 504537cd32..776caad62c 100644 --- a/ts/pnpm-lock.yaml +++ b/ts/pnpm-lock.yaml @@ -2206,6 +2206,49 @@ importers: specifier: ^5.2.0 version: 5.2.1(debug@4.4.1)(webpack-cli@5.1.4)(webpack@5.105.0) + packages/agents/onboarding: + dependencies: + '@typeagent/agent-sdk': + specifier: workspace:* + version: link:../../agentSdk + '@typeagent/dispatcher-types': + specifier: workspace:* + version: link:../../dispatcher/types + agent-dispatcher: + specifier: workspace:* + version: link:../../dispatcher/dispatcher + aiclient: + specifier: workspace:* + version: link:../../aiclient + default-agent-provider: + specifier: workspace:* + version: link:../../defaultAgentProvider + dispatcher-node-providers: + specifier: workspace:* + version: link:../../dispatcher/nodeProviders + typechat: + specifier: ^0.1.1 + version: 0.1.1(typescript@5.4.5)(zod@3.25.76) + devDependencies: + '@typeagent/action-schema-compiler': + specifier: workspace:* + version: link:../../actionSchemaCompiler + action-grammar-compiler: + specifier: workspace:* + version: link:../../actionGrammarCompiler + concurrently: + specifier: ^9.1.2 + version: 9.1.2 + prettier: + specifier: ^3.5.3 + version: 3.5.3 + rimraf: + specifier: ^6.0.1 + version: 6.0.1 + typescript: + specifier: ~5.4.5 + version: 5.4.5 + packages/agents/photo: dependencies: '@typeagent/agent-sdk': @@ -3313,6 +3356,9 @@ importers: music-local: specifier: workspace:* version: link:../agents/playerLocal + onboarding-agent: + specifier: workspace:* + version: link:../agents/onboarding photo-agent: specifier: workspace:* version: link:../agents/photo @@ -12140,10 +12186,6 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-scurry@2.0.1: - resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} - engines: {node: 20 || >=22} - path-scurry@2.0.2: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} @@ -16480,7 +16522,7 @@ snapshots: '@isaacs/fs-minipass@4.0.1': dependencies: - minipass: 7.1.2 + minipass: 7.1.3 '@istanbuljs/load-nyc-config@1.1.0': dependencies: @@ -21690,9 +21732,9 @@ snapshots: foreground-child: 3.3.1 jackspeak: 4.1.1 minimatch: 10.2.4 - minipass: 7.1.2 + minipass: 7.1.3 package-json-from-dist: 1.0.1 - path-scurry: 2.0.1 + path-scurry: 2.0.2 glob@13.0.6: dependencies: @@ -24199,7 +24241,7 @@ snapshots: minizlib@3.1.0: dependencies: - minipass: 7.1.2 + minipass: 7.1.3 mitt@3.0.1: {} @@ -24743,12 +24785,7 @@ snapshots: path-scurry@1.11.1: dependencies: lru-cache: 10.4.3 - minipass: 7.1.2 - - path-scurry@2.0.1: - dependencies: - lru-cache: 11.2.2 - minipass: 7.1.2 + minipass: 7.1.3 path-scurry@2.0.2: dependencies: @@ -26233,7 +26270,7 @@ snapshots: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 - minipass: 7.1.2 + minipass: 7.1.3 minizlib: 3.1.0 yallist: 5.0.0 From 1a7126e77c9e821bf8c2be6ae4df99f934b4c0c0 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Mon, 6 Apr 2026 10:41:05 -0700 Subject: [PATCH 04/33] updated pnpm version --- ts/pnpm-lock.yaml | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml index 3a750c99ad..52b06cc86c 100644 --- a/ts/pnpm-lock.yaml +++ b/ts/pnpm-lock.yaml @@ -2232,6 +2232,49 @@ importers: specifier: ^5.2.0 version: 5.2.1(debug@4.4.1)(webpack-cli@5.1.4)(webpack@5.105.0) + packages/agents/onboarding: + dependencies: + '@typeagent/agent-sdk': + specifier: workspace:* + version: link:../../agentSdk + '@typeagent/dispatcher-types': + specifier: workspace:* + version: link:../../dispatcher/types + agent-dispatcher: + specifier: workspace:* + version: link:../../dispatcher/dispatcher + aiclient: + specifier: workspace:* + version: link:../../aiclient + default-agent-provider: + specifier: workspace:* + version: link:../../defaultAgentProvider + dispatcher-node-providers: + specifier: workspace:* + version: link:../../dispatcher/nodeProviders + typechat: + specifier: ^0.1.1 + version: 0.1.1(typescript@5.4.5)(zod@3.25.76) + devDependencies: + '@typeagent/action-schema-compiler': + specifier: workspace:* + version: link:../../actionSchemaCompiler + action-grammar-compiler: + specifier: workspace:* + version: link:../../actionGrammarCompiler + concurrently: + specifier: ^9.1.2 + version: 9.1.2 + prettier: + specifier: ^3.5.3 + version: 3.5.3 + rimraf: + specifier: ^6.0.1 + version: 6.0.1 + typescript: + specifier: ~5.4.5 + version: 5.4.5 + packages/agents/photo: dependencies: '@typeagent/agent-sdk': @@ -3358,6 +3401,9 @@ importers: music-local: specifier: workspace:* version: link:../agents/playerLocal + onboarding-agent: + specifier: workspace:* + version: link:../agents/onboarding photo-agent: specifier: workspace:* version: link:../agents/photo From 51f85d1137ab4bb6189d7fe54fb0892c182ec414 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Mon, 6 Apr 2026 14:40:33 -0700 Subject: [PATCH 05/33] removed circular dependency from the onboarding agent --- ts/package.json | 2 +- ts/packages/agents/onboarding/package.json | 1 - .../src/discovery/discoveryHandler.ts | 1 - .../src/discovery/discoverySchema.ts | 4 --- .../src/grammarGen/grammarGenHandler.ts | 1 - .../src/grammarGen/grammarGenSchema.ts | 3 -- ts/packages/agents/onboarding/src/lib/llm.ts | 15 ++++---- .../onboarding/src/onboardingActionHandler.ts | 6 ++-- .../onboarding/src/onboardingManifest.json | 2 +- .../agents/onboarding/src/onboardingSchema.ts | 3 -- .../src/packaging/packagingHandler.ts | 4 --- .../src/packaging/packagingSchema.ts | 3 -- .../src/phraseGen/phraseGenSchema.agr | 2 +- .../src/phraseGen/phraseGenSchema.ts | 4 --- .../src/scaffolder/scaffolderHandler.ts | 3 -- .../src/scaffolder/scaffolderSchema.agr | 6 ++-- .../src/scaffolder/scaffolderSchema.ts | 6 +--- .../src/schemaGen/schemaGenHandler.ts | 1 - .../src/schemaGen/schemaGenSchema.ts | 4 --- .../onboarding/src/testing/testingHandler.ts | 34 +++++++++++++++---- .../onboarding/src/testing/testingSchema.ts | 3 -- ts/pnpm-lock.yaml | 3 -- 22 files changed, 44 insertions(+), 67 deletions(-) diff --git a/ts/package.json b/ts/package.json index cccba2bfe2..a7f65d7b26 100644 --- a/ts/package.json +++ b/ts/package.json @@ -63,7 +63,7 @@ "prettier": "^3.5.3", "shx": "^0.4.0" }, - "packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be", + "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319", "engines": { "node": ">=20", "pnpm": ">=10" diff --git a/ts/packages/agents/onboarding/package.json b/ts/packages/agents/onboarding/package.json index 81d4d861a3..a29bd1dba1 100644 --- a/ts/packages/agents/onboarding/package.json +++ b/ts/packages/agents/onboarding/package.json @@ -44,7 +44,6 @@ "@typeagent/dispatcher-types": "workspace:*", "agent-dispatcher": "workspace:*", "aiclient": "workspace:*", - "default-agent-provider": "workspace:*", "dispatcher-node-providers": "workspace:*", "typechat": "^0.1.1" }, diff --git a/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts b/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts index 1da0d27226..3a4564801f 100644 --- a/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts +++ b/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts @@ -11,7 +11,6 @@ import { ActionResult, } from "@typeagent/agent-sdk"; import { - createActionResultFromTextDisplay, createActionResultFromMarkdownDisplay, } from "@typeagent/agent-sdk/helpers/action"; import { DiscoveryActions } from "./discoverySchema.js"; diff --git a/ts/packages/agents/onboarding/src/discovery/discoverySchema.ts b/ts/packages/agents/onboarding/src/discovery/discoverySchema.ts index fdbaeddd10..44d27c9bcc 100644 --- a/ts/packages/agents/onboarding/src/discovery/discoverySchema.ts +++ b/ts/packages/agents/onboarding/src/discovery/discoverySchema.ts @@ -1,10 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -// Phase 1 — Discovery: enumerate the API surface of the target application. -// Actions in this phase ingest documentation or API specs and produce -// a list of candidate actions saved to ~/.typeagent/onboarding//discovery/api-surface.json - export type DiscoveryActions = | CrawlDocUrlAction | ParseOpenApiSpecAction diff --git a/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts b/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts index 2cb60ada15..57852d545e 100644 --- a/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts +++ b/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts @@ -11,7 +11,6 @@ import { ActionResult, } from "@typeagent/agent-sdk"; import { - createActionResultFromTextDisplay, createActionResultFromMarkdownDisplay, } from "@typeagent/agent-sdk/helpers/action"; import { GrammarGenActions } from "./grammarGenSchema.js"; diff --git a/ts/packages/agents/onboarding/src/grammarGen/grammarGenSchema.ts b/ts/packages/agents/onboarding/src/grammarGen/grammarGenSchema.ts index b0b9e91757..4f5f258734 100644 --- a/ts/packages/agents/onboarding/src/grammarGen/grammarGenSchema.ts +++ b/ts/packages/agents/onboarding/src/grammarGen/grammarGenSchema.ts @@ -1,9 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -// Phase 4 — Grammar Generation: produce a .agr grammar file from the -// approved TypeScript schema and sample phrases, then compile and validate it. - export type GrammarGenActions = | GenerateGrammarAction | CompileGrammarAction diff --git a/ts/packages/agents/onboarding/src/lib/llm.ts b/ts/packages/agents/onboarding/src/lib/llm.ts index d5aae8c7d0..e79e6b6d06 100644 --- a/ts/packages/agents/onboarding/src/lib/llm.ts +++ b/ts/packages/agents/onboarding/src/lib/llm.ts @@ -7,29 +7,28 @@ // // Credentials are read from ts/.env via the standard TypeAgent mechanism. -import { createChatModelDefault } from "aiclient"; -import type { ChatModel } from "aiclient"; +import { ChatModel, openai } from "aiclient"; export function getDiscoveryModel(): ChatModel { - return createChatModelDefault("onboarding:discovery"); + return openai.createChatModelDefault("onboarding:discovery"); } export function getPhraseGenModel(): ChatModel { - return createChatModelDefault("onboarding:phrasegen"); + return openai.createChatModelDefault("onboarding:phrasegen"); } export function getSchemaGenModel(): ChatModel { - return createChatModelDefault("onboarding:schemagen"); + return openai.createChatModelDefault("onboarding:schemagen"); } export function getGrammarGenModel(): ChatModel { - return createChatModelDefault("onboarding:grammargen"); + return openai.createChatModelDefault("onboarding:grammargen"); } export function getTestingModel(): ChatModel { - return createChatModelDefault("onboarding:testing"); + return openai.createChatModelDefault("onboarding:testing"); } export function getPackagingModel(): ChatModel { - return createChatModelDefault("onboarding:packaging"); + return openai.createChatModelDefault("onboarding:packaging"); } diff --git a/ts/packages/agents/onboarding/src/onboardingActionHandler.ts b/ts/packages/agents/onboarding/src/onboardingActionHandler.ts index 823fa07e51..a746f228e1 100644 --- a/ts/packages/agents/onboarding/src/onboardingActionHandler.ts +++ b/ts/packages/agents/onboarding/src/onboardingActionHandler.ts @@ -167,10 +167,10 @@ async function executeOnboardingAction( `Integration "${integrationName}" already exists (current phase: ${existing.currentPhase}). Use resumeOnboarding to continue.`, ); } - const state = await createWorkspace({ + await createWorkspace({ integrationName, - description, - apiType, + ...(description !== undefined ? { description } : undefined), + ...(apiType !== undefined ? { apiType } : undefined), }); return createActionResultFromMarkdownDisplay( `## Onboarding started: ${integrationName}\n\n` + diff --git a/ts/packages/agents/onboarding/src/onboardingManifest.json b/ts/packages/agents/onboarding/src/onboardingManifest.json index e699db6946..22a5d7381d 100644 --- a/ts/packages/agents/onboarding/src/onboardingManifest.json +++ b/ts/packages/agents/onboarding/src/onboardingManifest.json @@ -1,7 +1,7 @@ { "emojiChar": "🛠️", "description": "Agent for onboarding new applications and APIs into TypeAgent", - "defaultEnabled": false, + "defaultEnabled": true, "schema": { "description": "Top-level onboarding coordination: start, resume, and check status of integration onboarding workflows", "originalSchemaFile": "./onboardingSchema.ts", diff --git a/ts/packages/agents/onboarding/src/onboardingSchema.ts b/ts/packages/agents/onboarding/src/onboardingSchema.ts index 7be5badfa4..6980d3cfc2 100644 --- a/ts/packages/agents/onboarding/src/onboardingSchema.ts +++ b/ts/packages/agents/onboarding/src/onboardingSchema.ts @@ -1,9 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -// Top-level onboarding coordination actions. -// These actions manage the lifecycle of an integration onboarding workflow. - export type OnboardingActions = | StartOnboardingAction | ResumeOnboardingAction diff --git a/ts/packages/agents/onboarding/src/packaging/packagingHandler.ts b/ts/packages/agents/onboarding/src/packaging/packagingHandler.ts index cb584f50a7..9a848e6ea8 100644 --- a/ts/packages/agents/onboarding/src/packaging/packagingHandler.ts +++ b/ts/packages/agents/onboarding/src/packaging/packagingHandler.ts @@ -11,7 +11,6 @@ import { ActionResult, } from "@typeagent/agent-sdk"; import { - createActionResultFromTextDisplay, createActionResultFromMarkdownDisplay, } from "@typeagent/agent-sdk/helpers/action"; import { PackagingActions } from "./packagingSchema.js"; @@ -19,13 +18,10 @@ import { loadState, updatePhase, readArtifact, - writeArtifactJson, - getWorkspacePath, } from "../lib/workspace.js"; import { spawn } from "child_process"; import path from "path"; import fs from "fs/promises"; -import os from "os"; export async function executePackagingAction( action: TypeAgentAction, diff --git a/ts/packages/agents/onboarding/src/packaging/packagingSchema.ts b/ts/packages/agents/onboarding/src/packaging/packagingSchema.ts index eb8d1d3618..c597bb8caf 100644 --- a/ts/packages/agents/onboarding/src/packaging/packagingSchema.ts +++ b/ts/packages/agents/onboarding/src/packaging/packagingSchema.ts @@ -1,9 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -// Phase 7 — Packaging: build and validate the scaffolded agent package, -// then register it with the TypeAgent dispatcher for end-user testing. - export type PackagingActions = | PackageAgentAction | ValidatePackageAction; diff --git a/ts/packages/agents/onboarding/src/phraseGen/phraseGenSchema.agr b/ts/packages/agents/onboarding/src/phraseGen/phraseGenSchema.agr index cc3fd1cd12..ecfb22bde7 100644 --- a/ts/packages/agents/onboarding/src/phraseGen/phraseGenSchema.agr +++ b/ts/packages/agents/onboarding/src/phraseGen/phraseGenSchema.agr @@ -16,7 +16,7 @@ integrationName } } - | generate $(phrasesPerAction:word) phrases (per action)? for $(integrationName:wildcard) -> { + | generate $(phrasesPerAction:number) phrases (per action)? for $(integrationName:wildcard) -> { actionName: "generatePhrases", parameters: { integrationName, diff --git a/ts/packages/agents/onboarding/src/phraseGen/phraseGenSchema.ts b/ts/packages/agents/onboarding/src/phraseGen/phraseGenSchema.ts index 2bcf488e31..f33b7def18 100644 --- a/ts/packages/agents/onboarding/src/phraseGen/phraseGenSchema.ts +++ b/ts/packages/agents/onboarding/src/phraseGen/phraseGenSchema.ts @@ -1,10 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -// Phase 2 — Phrase Generation: produce natural language sample phrases -// for each discovered action. Phrases are used to train and validate -// the grammar and schema in later phases. - export type PhraseGenActions = | GeneratePhrasesAction | AddPhraseAction diff --git a/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts b/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts index c81620db70..6cae8114ba 100644 --- a/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts +++ b/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts @@ -11,7 +11,6 @@ import { ActionResult, } from "@typeagent/agent-sdk"; import { - createActionResultFromTextDisplay, createActionResultFromMarkdownDisplay, } from "@typeagent/agent-sdk/helpers/action"; import { ScaffolderActions } from "./scaffolderSchema.js"; @@ -20,11 +19,9 @@ import { updatePhase, writeArtifact, readArtifact, - getWorkspacePath, } from "../lib/workspace.js"; import fs from "fs/promises"; import path from "path"; -import os from "os"; // Default output root within the TypeAgent repo const AGENTS_DIR = path.resolve( diff --git a/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.agr b/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.agr index e50d9c3b16..a7752c25f5 100644 --- a/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.agr +++ b/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.agr @@ -16,18 +16,18 @@ } }; - = scaffold (the)? $(integrationName:wildcard) (plugin | add-in | extension) -> { + = scaffold (the)? $(integrationName:wildcard) (plugin | extension) -> { actionName: "scaffoldPlugin", parameters: { integrationName, template: "rest-client" } } - | (create | generate) (the)? $(integrationName:wildcard) $(template:word) (plugin | extension | add-in) -> { + | (create | generate) (the)? $(integrationName:wildcard) (plugin | extension) -> { actionName: "scaffoldPlugin", parameters: { integrationName, - template + template: "rest-client" } }; diff --git a/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.ts b/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.ts index f022977090..f6ae49263c 100644 --- a/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.ts +++ b/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.ts @@ -1,10 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -// Phase 5 — Scaffolder: stamp out the complete TypeAgent agent package -// from the approved schema, grammar, and config. Output goes to -// ~/.typeagent/onboarding//scaffolder/agent/ - export type ScaffolderActions = | ScaffoldAgentAction | ScaffoldPluginAction @@ -34,5 +30,5 @@ export type ScaffoldPluginAction = { export type ListTemplatesAction = { actionName: "listTemplates"; - parameters: Record; + parameters: {}; }; diff --git a/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts b/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts index 98625b1cec..238e2f0c62 100644 --- a/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts +++ b/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts @@ -11,7 +11,6 @@ import { ActionResult, } from "@typeagent/agent-sdk"; import { - createActionResultFromTextDisplay, createActionResultFromMarkdownDisplay, } from "@typeagent/agent-sdk/helpers/action"; import { SchemaGenActions } from "./schemaGenSchema.js"; diff --git a/ts/packages/agents/onboarding/src/schemaGen/schemaGenSchema.ts b/ts/packages/agents/onboarding/src/schemaGen/schemaGenSchema.ts index bb062892e4..397f9c17cc 100644 --- a/ts/packages/agents/onboarding/src/schemaGen/schemaGenSchema.ts +++ b/ts/packages/agents/onboarding/src/schemaGen/schemaGenSchema.ts @@ -1,10 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -// Phase 3 — Schema Generation: produce a TypeScript action schema file -// that maps natural language user requests to the target API surface. -// Output is saved to ~/.typeagent/onboarding//schemaGen/schema.ts - export type SchemaGenActions = | GenerateSchemaAction | RefineSchemaAction diff --git a/ts/packages/agents/onboarding/src/testing/testingHandler.ts b/ts/packages/agents/onboarding/src/testing/testingHandler.ts index f3341e4be1..ddb0e55fec 100644 --- a/ts/packages/agents/onboarding/src/testing/testingHandler.ts +++ b/ts/packages/agents/onboarding/src/testing/testingHandler.ts @@ -26,8 +26,12 @@ import { import { getTestingModel } from "../lib/llm.js"; import { PhraseSet } from "../phraseGen/phraseGenHandler.js"; import { createDispatcher } from "agent-dispatcher"; -import { getDefaultAppAgentProviders } from "default-agent-provider"; -import { getFsStorageProvider } from "dispatcher-node-providers"; +import { + createNpmAppAgentProvider, + getFsStorageProvider, +} from "dispatcher-node-providers"; +import fs from "node:fs"; +import path from "node:path"; import { getInstanceDir } from "agent-dispatcher/helpers/data"; import type { ClientIO, @@ -442,12 +446,26 @@ function createCapturingClientIO(buffer: string[]): ClientIO { } satisfies ClientIO; } -// Create a dispatcher wired to the default agent providers. +// Build a provider containing only the externally-registered agents. // The scaffolded agent must be registered in the TypeAgent config before // running tests (use `packageAgent --register` or add manually to config.json). +function getExternalAppAgentProviders(instanceDir: string) { + const configPath = path.join(instanceDir, "externalAgentsConfig.json"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const agents = fs.existsSync(configPath) + ? (JSON.parse(fs.readFileSync(configPath, "utf8")) as any).agents ?? {} + : {}; + return [ + createNpmAppAgentProvider( + agents, + path.join(instanceDir, "externalagents/package.json"), + ), + ]; +} + async function createTestDispatcher() { const instanceDir = getInstanceDir(); - const appAgentProviders = getDefaultAppAgentProviders(instanceDir); + const appAgentProviders = getExternalAppAgentProviders(instanceDir); const buffer: string[] = []; const clientIO = createCapturingClientIO(buffer); @@ -502,11 +520,13 @@ async function runSingleTest( return { phrase: tc.phrase, expectedActionName: tc.expectedActionName, - actualActionName, + ...(actualActionName !== undefined ? { actualActionName } : undefined), passed, - error: passed + ...(passed ? undefined - : `Expected "${tc.expectedActionName}", got "${actualActionName ?? "no action"}"`, + : { + error: `Expected "${tc.expectedActionName}", got "${actualActionName ?? "no action"}"`, + }), }; } diff --git a/ts/packages/agents/onboarding/src/testing/testingSchema.ts b/ts/packages/agents/onboarding/src/testing/testingSchema.ts index efd7f89ca3..1b9e6e3788 100644 --- a/ts/packages/agents/onboarding/src/testing/testingSchema.ts +++ b/ts/packages/agents/onboarding/src/testing/testingSchema.ts @@ -1,9 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -// Phase 6 — Testing: generate test cases from sample phrases and run a -// phrase-to-action validation loop, proposing repairs for failures. - export type TestingActions = | GenerateTestsAction | RunTestsAction diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml index 52b06cc86c..da9b778107 100644 --- a/ts/pnpm-lock.yaml +++ b/ts/pnpm-lock.yaml @@ -2246,9 +2246,6 @@ importers: aiclient: specifier: workspace:* version: link:../../aiclient - default-agent-provider: - specifier: workspace:* - version: link:../../defaultAgentProvider dispatcher-node-providers: specifier: workspace:* version: link:../../dispatcher/nodeProviders From 401189b640557da73748d91772170d08e4c8fd73 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Mon, 6 Apr 2026 20:06:51 -0700 Subject: [PATCH 06/33] allowed continuing loading of agents when there's a problem loading an agent --- .../commandExecutor/src/commandServer.ts | 23 ++++++++++-- ts/pnpm-lock.yaml | 36 ++++++++++++++++++- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/ts/packages/commandExecutor/src/commandServer.ts b/ts/packages/commandExecutor/src/commandServer.ts index ba9e8cfbee..68a3fd12e2 100644 --- a/ts/packages/commandExecutor/src/commandServer.ts +++ b/ts/packages/commandExecutor/src/commandServer.ts @@ -367,6 +367,12 @@ export class CommandServer { clientIO, this.agentServerUrl, { filter: true }, + () => { + this.logger.log( + "Dispatcher connection dropped, will reconnect...", + ); + this.dispatcher = null; + }, ); this.logger.log( `Connected to TypeAgent dispatcher at ${this.agentServerUrl}`, @@ -629,8 +635,21 @@ export class CommandServer { if (!this.dispatcher) { return []; } - const schemas = await this.dispatcher.getAgentSchemas(agentName); - return schemas.filter((a) => !SKIP_AGENTS.has(a.name)); + try { + const schemas = await this.dispatcher.getAgentSchemas(agentName); + return schemas.filter((a) => !SKIP_AGENTS.has(a.name)); + } catch (error) { + if ( + error instanceof Error && + error.message.includes("Agent channel disconnected") + ) { + this.logger.log( + "Agent channel disconnected during getAgentSchemas, clearing dispatcher", + ); + this.dispatcher = null; + } + return []; + } } private async discoverAgents(request: { diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml index da9b778107..505bf82b88 100644 --- a/ts/pnpm-lock.yaml +++ b/ts/pnpm-lock.yaml @@ -2392,6 +2392,37 @@ importers: specifier: ~5.4.5 version: 5.4.5 + packages/agents/powerpoint: + dependencies: + '@typeagent/agent-sdk': + specifier: workspace:* + version: link:../../agentSdk + ws: + specifier: ^8.18.0 + version: 8.19.0 + devDependencies: + '@typeagent/action-schema-compiler': + specifier: workspace:* + version: link:../../actionSchemaCompiler + '@types/ws': + specifier: ^8.5.12 + version: 8.18.1 + action-grammar-compiler: + specifier: workspace:* + version: link:../../actionGrammarCompiler + concurrently: + specifier: ^9.1.2 + version: 9.1.2 + prettier: + specifier: ^3.5.3 + version: 3.5.3 + rimraf: + specifier: ^6.0.1 + version: 6.0.1 + typescript: + specifier: ~5.4.5 + version: 5.4.5 + packages/agents/scriptflow: dependencies: '@anthropic-ai/claude-agent-sdk': @@ -3404,6 +3435,9 @@ importers: photo-agent: specifier: workspace:* version: link:../agents/photo + powerpoint-agent: + specifier: workspace:* + version: link:../agents/powerpoint proper-lockfile: specifier: ^4.1.2 version: 4.1.2 @@ -23337,7 +23371,7 @@ snapshots: whatwg-encoding: 2.0.0 whatwg-mimetype: 3.0.0 whatwg-url: 11.0.0 - ws: 8.18.2 + ws: 8.19.0 xml-name-validator: 4.0.0 transitivePeerDependencies: - bufferutil From c39039c8ab9161e8bb0438ffbb8a100601870b44 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Mon, 6 Apr 2026 20:07:34 -0700 Subject: [PATCH 07/33] getAgentSchemas now waits for all agents to finish loading their schemas before returning --- .../dispatcher/src/context/appAgentManager.ts | 22 +++++++++++++++++++ .../dispatcher/dispatcher/src/dispatcher.ts | 18 ++++++++++----- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/ts/packages/dispatcher/dispatcher/src/context/appAgentManager.ts b/ts/packages/dispatcher/dispatcher/src/context/appAgentManager.ts index 8b48148432..4e5e0b1120 100644 --- a/ts/packages/dispatcher/dispatcher/src/context/appAgentManager.ts +++ b/ts/packages/dispatcher/dispatcher/src/context/appAgentManager.ts @@ -123,6 +123,7 @@ export class AppAgentManager implements ActionConfigProvider { private readonly loadingSchemas = new Set(); private readonly flowRegistry = new Map(); private readonly transientAgents: Record = {}; + private readyWaiters: Array<() => void> = []; private readonly actionSemanticMap?: ActionSchemaSemanticMap; private readonly actionSchemaFileCache: ActionSchemaFileCache; private nextPortIndex = 0; @@ -388,6 +389,8 @@ export class AppAgentManager implements ActionConfigProvider { } catch (e: any) { record.schemaErrors.set(schemaName, e); this.loadingSchemas.delete(schemaName); + } finally { + this.notifyReadyIfDone(); } } } @@ -720,6 +723,25 @@ export class AppAgentManager implements ActionConfigProvider { return Array.from(this.actionConfigs.values()); } + /** Resolves immediately if no schemas are loading, otherwise waits until all pending async schema loads complete. */ + public waitUntilReady(): Promise { + if (this.loadingSchemas.size === 0) { + return Promise.resolve(); + } + return new Promise((resolve) => { + this.readyWaiters.push(resolve); + }); + } + + private notifyReadyIfDone(): void { + if (this.loadingSchemas.size === 0 && this.readyWaiters.length > 0) { + const waiters = this.readyWaiters.splice(0); + for (const resolve of waiters) { + resolve(); + } + } + } + public getAppAgent(appAgentName: string): AppAgent { const record = this.getRecord(appAgentName); if (record.appAgent === undefined) { diff --git a/ts/packages/dispatcher/dispatcher/src/dispatcher.ts b/ts/packages/dispatcher/dispatcher/src/dispatcher.ts index 3560ca6db3..34eef51526 100644 --- a/ts/packages/dispatcher/dispatcher/src/dispatcher.ts +++ b/ts/packages/dispatcher/dispatcher/src/dispatcher.ts @@ -147,16 +147,17 @@ function extractActions( return actions; } -function getAgentSchemas( +async function getAgentSchemas( context: CommandHandlerContext, agentName?: string, -): AgentSchemaInfo[] { +): Promise { + await context.agents.waitUntilReady(); const configs = context.agents.getActionConfigs(); // Group configs by top-level agent name (part before first '.') const agentMap = new Map(); for (const config of configs) { const topName = config.schemaName.split(".")[0]; - if (agentName !== undefined && topName !== agentName) continue; + if (agentName != null && topName !== agentName) continue; const list = agentMap.get(topName) ?? []; list.push(config); agentMap.set(topName, list); @@ -173,9 +174,14 @@ function getAgentSchemas( const subSchemas: AgentSubSchemaInfo[] = []; for (const config of sorted) { - const schemaFile = context.agents.tryGetActionSchemaFile( - config.schemaName, - ); + let schemaFile; + try { + schemaFile = context.agents.tryGetActionSchemaFile( + config.schemaName, + ); + } catch { + continue; + } const actions = schemaFile ? extractActions(schemaFile.parsedActionSchema.actionSchemas) : []; From c99e81db79eebdbb37cff3cdbdc197cddbde38c8 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Mon, 6 Apr 2026 20:08:40 -0700 Subject: [PATCH 08/33] prevent dispatcher deadlock, load last session by default when th agent server starts --- ts/packages/agentServer/server/src/server.ts | 5 ++ .../agentServer/server/src/sessionManager.ts | 60 ++++++++++++++----- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/ts/packages/agentServer/server/src/server.ts b/ts/packages/agentServer/server/src/server.ts index b372852cc9..df2fef379e 100644 --- a/ts/packages/agentServer/server/src/server.ts +++ b/ts/packages/agentServer/server/src/server.ts @@ -60,6 +60,11 @@ async function main() { instanceDir, ); + // Pre-initialize the default session dispatcher before accepting clients, + // so the first joinSession call is fast and concurrent joinSession calls + // don't race to initialize the same dispatcher. + await sessionManager.prewarmMostRecentSession(); + const wss = await createWebSocketChannelServer( { port: 8999 }, (channelProvider: ChannelProvider, closeFn: () => void) => { diff --git a/ts/packages/agentServer/server/src/sessionManager.ts b/ts/packages/agentServer/server/src/sessionManager.ts index 0cb5523805..ef0c32fbbe 100644 --- a/ts/packages/agentServer/server/src/sessionManager.ts +++ b/ts/packages/agentServer/server/src/sessionManager.ts @@ -34,6 +34,7 @@ type SessionRecord = { createdAt: string; lastActiveAt: number; sharedDispatcher: SharedDispatcher | undefined; // undefined = not yet restored + sharedDispatcherP: Promise | undefined; // in-progress init idleTimer: ReturnType | undefined; }; @@ -49,6 +50,12 @@ export type SessionManager = { * session, creating a default one if none exist. */ resolveSessionId(sessionId: string | undefined): Promise; + /** + * Pre-initialize the most recently active session's dispatcher so it is + * ready before the first client connects. If no sessions exist, a "default" + * session is created. Safe to call multiple times. + */ + prewarmMostRecentSession(): Promise; joinSession( sessionId: string, clientIO: ClientIO, @@ -89,6 +96,7 @@ export async function createSessionManager( createdAt: entry.createdAt, lastActiveAt: 0, sharedDispatcher: undefined, // lazy restore + sharedDispatcherP: undefined, idleTimer: undefined, }); } @@ -143,22 +151,35 @@ export async function createSessionManager( return path.join(sessionsDir, sessionId); } - async function ensureDispatcher( - record: SessionRecord, - ): Promise { - if (record.sharedDispatcher === undefined) { + function ensureDispatcher(record: SessionRecord): Promise { + if (record.sharedDispatcher !== undefined) { + return Promise.resolve(record.sharedDispatcher); + } + if (record.sharedDispatcherP === undefined) { const persistDir = getSessionPersistDir(record.sessionId); - await fs.promises.mkdir(persistDir, { recursive: true }); - record.sharedDispatcher = await createSharedDispatcher(hostName, { - ...baseOptions, - persistDir, - persistSession: true, - }); - debugSession( - `Dispatcher initialized for session "${record.name}" (${record.sessionId})`, - ); + record.sharedDispatcherP = fs.promises + .mkdir(persistDir, { recursive: true }) + .then(() => + createSharedDispatcher(hostName, { + ...baseOptions, + persistDir, + persistSession: true, + }), + ) + .then((dispatcher) => { + record.sharedDispatcher = dispatcher; + record.sharedDispatcherP = undefined; + debugSession( + `Dispatcher initialized for session "${record.name}" (${record.sessionId})`, + ); + return dispatcher; + }) + .catch((e) => { + record.sharedDispatcherP = undefined; + throw e; + }); } - return record.sharedDispatcher; + return record.sharedDispatcherP; } function cancelIdleTimer(record: SessionRecord): void { @@ -237,6 +258,7 @@ export async function createSessionManager( createdAt, lastActiveAt: Date.now(), sharedDispatcher: undefined, + sharedDispatcherP: undefined, idleTimer: undefined, }; sessions.set(sessionId, record); @@ -267,6 +289,16 @@ export async function createSessionManager( return info.sessionId; }, + async prewarmMostRecentSession(): Promise { + const sessionId = await manager.resolveSessionId(undefined); + const record = sessions.get(sessionId)!; + cancelIdleTimer(record); + await ensureDispatcher(record); + debugSession( + `Pre-warmed dispatcher for session "${record.name}" (${sessionId})`, + ); + }, + async joinSession( sessionId: string, clientIO: ClientIO, From 09bf6eedccf141c3908bd63746e78ea4510a190b Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Mon, 6 Apr 2026 22:05:42 -0700 Subject: [PATCH 09/33] added the ability to restart agent mcp server --- .../commandExecutor/src/commandServer.ts | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/ts/packages/commandExecutor/src/commandServer.ts b/ts/packages/commandExecutor/src/commandServer.ts index 68a3fd12e2..ea09903be0 100644 --- a/ts/packages/commandExecutor/src/commandServer.ts +++ b/ts/packages/commandExecutor/src/commandServer.ts @@ -528,6 +528,61 @@ export class CommandServer { request.message ? "PONG: " + request.message : "pong", ), ); + + this.server.registerTool( + "restart", + { + inputSchema: { + mode: z + .enum(["reconnect", "full"]) + .optional() + .describe( + 'reconnect: disconnect and reconnect to the agent server (default). full: exit the MCP server process so the client can restart it (picks up new MCP server code).', + ), + }, + description: + "Restart the MCP server connection. Use 'reconnect' mode (default) to reconnect to the agent server after it has been restarted. Use 'full' mode to exit the MCP server process entirely so the MCP client can restart it with updated code.", + }, + async (request: { mode?: "reconnect" | "full" | undefined }) => + this.restart(request.mode ?? "reconnect"), + ); + } + + private async restart( + mode: "reconnect" | "full", + ): Promise { + if (mode === "full") { + this.logger.log("Full restart requested — exiting process."); + // Give time for the response to be sent before exiting + setTimeout(() => process.exit(0), 500); + return toolResult( + "MCP server is shutting down. The MCP client should restart it automatically.", + ); + } + + // Reconnect mode: disconnect and reconnect to the agent server + this.logger.log("Reconnect requested — disconnecting from dispatcher."); + if (this.dispatcher) { + try { + await this.dispatcher.close(); + } catch (error) { + this.logger.error("Error closing dispatcher", error); + } + this.dispatcher = null; + } + + this.logger.log("Reconnecting to dispatcher..."); + await this.connectToDispatcher(); + + if (this.dispatcher) { + return toolResult( + `Reconnected to agent server at ${this.agentServerUrl}.`, + ); + } else { + return toolResult( + `Failed to reconnect to agent server at ${this.agentServerUrl}. Will retry automatically.`, + ); + } } // ── Tool implementations ───────────────────────────────────────────────── From eab0bc376de3973e05a577cb29b33e7354a8adc2 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Mon, 6 Apr 2026 22:06:13 -0700 Subject: [PATCH 10/33] fixed json response format --- .../src/grammarGen/grammarGenHandler.ts | 9 ++++++- .../src/schemaGen/schemaGenHandler.ts | 11 ++++++-- .../onboarding/src/testing/testingHandler.ts | 27 ++++++++++++++----- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts b/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts index 57852d545e..6083eaaf5f 100644 --- a/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts +++ b/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts @@ -170,7 +170,7 @@ function buildGrammarPrompt( "The file must end with:\n" + " import { ActionType } from \"./schemaFile.ts\";\n" + " : ActionType = | | ...;\n\n" + - "Return only the .agr file content.", + "Respond in JSON format. Return a JSON object with a single `grammar` key containing the .agr file content as a string.", }, { role: "user", @@ -184,6 +184,13 @@ function buildGrammarPrompt( } function extractGrammarContent(llmResponse: string): string { + // Try to parse as JSON first (when using json_object response format) + try { + const parsed = JSON.parse(llmResponse); + if (parsed.grammar) return parsed.grammar.trim(); + } catch { + // Not JSON, fall through to other extraction methods + } const fenceMatch = llmResponse.match(/```(?:agr)?\n([\s\S]*?)```/); if (fenceMatch) return fenceMatch[1].trim(); return llmResponse.trim(); diff --git a/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts b/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts index 238e2f0c62..de2c26b565 100644 --- a/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts +++ b/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts @@ -91,7 +91,7 @@ async function handleRefineSchema( role: "system" as const, content: "You are a TypeScript expert. Modify the given TypeAgent action schema according to the instructions. " + - "Preserve all copyright headers and existing structure. Return only the updated TypeScript file content.", + "Preserve all copyright headers and existing structure. Respond in JSON format. Return a JSON object with a single `schema` key containing the updated TypeScript file content as a string.", }, { role: "user" as const, @@ -171,7 +171,7 @@ function buildSchemaPrompt( "- Parameters use camelCase names\n" + "- Optional parameters use `?: type` syntax\n" + "- Include the copyright header\n" + - "Return only the TypeScript file content.", + "Respond in JSON format. Return a JSON object with a single `schema` key containing the TypeScript file content as a string.", }, { role: "user", @@ -185,6 +185,13 @@ function buildSchemaPrompt( } function extractTypeScript(llmResponse: string): string { + // Try to parse as JSON first (when using json_object response format) + try { + const parsed = JSON.parse(llmResponse); + if (parsed.schema) return parsed.schema.trim(); + } catch { + // Not JSON, fall through to other extraction methods + } // Strip markdown code fences if present const fenceMatch = llmResponse.match(/```(?:typescript|ts)?\n([\s\S]*?)```/); if (fenceMatch) return fenceMatch[1].trim(); diff --git a/ts/packages/agents/onboarding/src/testing/testingHandler.ts b/ts/packages/agents/onboarding/src/testing/testingHandler.ts index ddb0e55fec..604b1db291 100644 --- a/ts/packages/agents/onboarding/src/testing/testingHandler.ts +++ b/ts/packages/agents/onboarding/src/testing/testingHandler.ts @@ -312,17 +312,32 @@ async function handleProposeRepair( return { error: `Repair proposal failed: ${result.message}` }; } + // Try to parse as JSON first (when using json_object response format) + let responseText = result.data; + let schemaFromJson: string | undefined; + let grammarFromJson: string | undefined; + try { + const parsed = JSON.parse(result.data); + responseText = parsed.explanation || result.data; + schemaFromJson = parsed.schema; + grammarFromJson = parsed.grammar; + } catch { + // Not JSON, fall through to regex extraction + } + const repair: ProposedRepair = { integrationName, proposedAt: new Date().toISOString(), - rationale: result.data, + rationale: responseText, }; // Extract suggested schema and grammar changes from the response - const schemaMatch = result.data.match(/```typescript([\s\S]*?)```/); - const grammarMatch = result.data.match(/```(?:agr)?([\s\S]*?)```/); - if (schemaMatch) repair.schemaChanges = schemaMatch[1].trim(); - if (grammarMatch) repair.grammarChanges = grammarMatch[1].trim(); + const schemaMatch = schemaFromJson ? null : result.data.match(/```typescript([\s\S]*?)```/); + const grammarMatch = grammarFromJson ? null : result.data.match(/```(?:agr)?([\s\S]*?)```/); + if (schemaFromJson) repair.schemaChanges = schemaFromJson.trim(); + else if (schemaMatch) repair.schemaChanges = schemaMatch[1].trim(); + if (grammarFromJson) repair.grammarChanges = grammarFromJson.trim(); + else if (grammarMatch) repair.grammarChanges = grammarMatch[1].trim(); await writeArtifactJson( integrationName, @@ -551,7 +566,7 @@ function buildRepairPrompt( "You are a TypeAgent grammar and schema expert. Analyze failing phrase-to-action test cases " + "and propose specific fixes to the TypeScript schema and/or .agr grammar file. " + "Explain what is wrong and why your changes will fix it. " + - "Provide the updated TypeScript schema in a ```typescript block and/or the updated grammar in a ```agr block.", + "Respond in JSON format. Return a JSON object with optional `schema` and `grammar` keys containing the updated file contents as strings, and an `explanation` key describing the fixes.", }, { role: "user", From e2975c596a01e4451c370d41a931aceb213dd470 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Mon, 6 Apr 2026 22:12:56 -0700 Subject: [PATCH 11/33] fixed path resolution --- .../onboarding/src/grammarGen/grammarGenHandler.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts b/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts index 6083eaaf5f..4520848e1f 100644 --- a/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts +++ b/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts @@ -27,6 +27,7 @@ import { ApiSurface } from "../discovery/discoveryHandler.js"; import { PhraseSet } from "../phraseGen/phraseGenHandler.js"; import { spawn } from "child_process"; import path from "path"; +import { fileURLToPath } from "url"; export async function executeGrammarGenAction( action: TypeAgentAction, @@ -91,8 +92,17 @@ async function handleCompileGrammar(integrationName: string): Promise { + // Resolve agc from the package's own node_modules/.bin + const pkgDir = path.resolve( + fileURLToPath(import.meta.url), "..", "..", "..", + ); + const binDir = path.join(pkgDir, "node_modules", ".bin"); + const env = { ...process.env, PATH: binDir + path.delimiter + (process.env.PATH ?? "") }; + const proc = spawn("agc", ["-i", grammarPath, "-o", outputPath], { stdio: ["ignore", "pipe", "pipe"], + env, + shell: true, }); let stdout = ""; From 5e9f54aaf6b2ac32ce41b1a71890885b25d03e57 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Mon, 6 Apr 2026 22:13:30 -0700 Subject: [PATCH 12/33] updated lock file --- ts/pnpm-lock.yaml | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml index 505bf82b88..646c3a362d 100644 --- a/ts/pnpm-lock.yaml +++ b/ts/pnpm-lock.yaml @@ -2392,37 +2392,6 @@ importers: specifier: ~5.4.5 version: 5.4.5 - packages/agents/powerpoint: - dependencies: - '@typeagent/agent-sdk': - specifier: workspace:* - version: link:../../agentSdk - ws: - specifier: ^8.18.0 - version: 8.19.0 - devDependencies: - '@typeagent/action-schema-compiler': - specifier: workspace:* - version: link:../../actionSchemaCompiler - '@types/ws': - specifier: ^8.5.12 - version: 8.18.1 - action-grammar-compiler: - specifier: workspace:* - version: link:../../actionGrammarCompiler - concurrently: - specifier: ^9.1.2 - version: 9.1.2 - prettier: - specifier: ^3.5.3 - version: 3.5.3 - rimraf: - specifier: ^6.0.1 - version: 6.0.1 - typescript: - specifier: ~5.4.5 - version: 5.4.5 - packages/agents/scriptflow: dependencies: '@anthropic-ai/claude-agent-sdk': @@ -3435,9 +3404,6 @@ importers: photo-agent: specifier: workspace:* version: link:../agents/photo - powerpoint-agent: - specifier: workspace:* - version: link:../agents/powerpoint proper-lockfile: specifier: ^4.1.2 version: 4.1.2 From 8c828d10e712bbe2e3e8207569c37f9f7233df81 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Tue, 7 Apr 2026 23:04:10 -0700 Subject: [PATCH 13/33] added sub-schema instructions --- .../src/discovery/discoveryHandler.ts | 98 +++++++ .../src/grammarGen/grammarGenHandler.ts | 26 +- .../src/scaffolder/scaffolderHandler.ts | 272 ++++++++++++++++-- .../src/schemaGen/schemaGenHandler.ts | 5 +- 4 files changed, 371 insertions(+), 30 deletions(-) diff --git a/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts b/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts index 3a4564801f..8880ab226e 100644 --- a/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts +++ b/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts @@ -359,10 +359,108 @@ async function handleApproveApiSurface( await writeArtifactJson(integrationName, "discovery", "api-surface.json", updated); await updatePhase(integrationName, "discovery", { status: "approved" }); + // If many actions, recommend sub-schema categorization + let subSchemaNote = ""; + if (approved.length > 20) { + subSchemaNote = await generateSubSchemaRecommendation( + integrationName, + approved, + ); + } + return createActionResultFromMarkdownDisplay( `## API surface approved: ${integrationName}\n\n` + `**Approved actions:** ${approved.length}\n\n` + approved.map((a) => `- \`${a.name}\`: ${a.description}`).join("\n") + + subSchemaNote + `\n\n**Next step:** Phase 2 — use \`generatePhrases\` to create natural language samples.`, ); } + +// When the approved action count exceeds 20, ask the LLM to categorize them +// into logical groups and save a sub-schema-groups.json artifact so that the +// scaffolder phase can generate sub-action manifests. +type SubSchemaGroup = { + name: string; + description: string; + actions: string[]; +}; + +type SubSchemaSuggestion = { + recommended: boolean; + groups: SubSchemaGroup[]; +}; + +async function generateSubSchemaRecommendation( + integrationName: string, + approved: DiscoveredAction[], +): Promise { + const model = getDiscoveryModel(); + const actionList = approved + .map((a) => `- ${a.name}: ${a.description}`) + .join("\n"); + + const prompt = [ + { + role: "system" as const, + content: + "You are an API architect. Given a list of API actions, categorize them " + + "into logical groups suitable for sub-schema separation in a TypeAgent agent. " + + "Each group should have a short camelCase name, a description, and the list of action names belonging to it. " + + "Every action must appear in exactly one group. Aim for 3-7 groups. " + + "Return ONLY a JSON array of objects with keys: name, description, actions.", + }, + { + role: "user" as const, + content: + `Categorize these ${approved.length} actions for the "${integrationName}" integration into logical sub-schema groups:\n\n${actionList}`, + }, + ]; + + const result = await model.complete(prompt); + if (!result.success) { + // Non-fatal — just skip the recommendation + return "\n\n> **Note:** Could not generate sub-schema recommendation (LLM error). You can still proceed."; + } + + let groups: SubSchemaGroup[] = []; + try { + const jsonMatch = result.data.match(/\[[\s\S]*\]/); + if (jsonMatch) { + groups = JSON.parse(jsonMatch[0]); + } + } catch { + return "\n\n> **Note:** Could not parse sub-schema recommendation. You can still proceed."; + } + + if (groups.length === 0) { + return ""; + } + + const suggestion: SubSchemaSuggestion = { + recommended: true, + groups, + }; + + await writeArtifactJson( + integrationName, + "discovery", + "sub-schema-groups.json", + suggestion, + ); + + const groupSummary = groups + .map( + (g) => + `- **${g.name}** (${g.actions.length} actions): ${g.description}`, + ) + .join("\n"); + + return ( + `\n\n---\n### Sub-schema recommendation\n\n` + + `With **${approved.length} actions**, we recommend splitting into sub-schemas for better organization:\n\n` + + groupSummary + + `\n\nThis grouping has been saved to \`discovery/sub-schema-groups.json\`. ` + + `The scaffolder will use it to generate separate schema and grammar files per group.` + ); +} diff --git a/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts b/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts index 4520848e1f..d307c48c70 100644 --- a/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts +++ b/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts @@ -26,6 +26,7 @@ import { getGrammarGenModel } from "../lib/llm.js"; import { ApiSurface } from "../discovery/discoveryHandler.js"; import { PhraseSet } from "../phraseGen/phraseGenHandler.js"; import { spawn } from "child_process"; +import fs from "fs/promises"; import path from "path"; import { fileURLToPath } from "url"; @@ -91,6 +92,15 @@ async function handleCompileGrammar(integrationName: string): Promise { // Resolve agc from the package's own node_modules/.bin const pkgDir = path.resolve( @@ -172,12 +182,22 @@ function buildGrammarPrompt( " = pattern -> { actionName: \"name\", parameters: { ... } }\n" + " | alternative -> { ... };\n\n" + "Pattern syntax:\n" + - " - $(paramName:wildcard) captures 1+ words\n" + - " - $(paramName:word) captures exactly 1 word\n" + + " - $(paramName:wildcard) captures 1+ words into a variable\n" + + " - $(paramName:word) captures exactly 1 word into a variable\n" + " - (optional)? makes tokens optional\n" + " - word matches a literal word\n" + " - | separates alternatives\n\n" + - "The file must end with:\n" + + "IMPORTANT: In the action output object after ->, reference captured parameters by BARE NAME only, NOT with $() syntax.\n" + + "Example:\n" + + " = add $(item:wildcard) to (the)? $(listName:wildcard) list -> {\n" + + " actionName: \"addItems\",\n" + + " parameters: {\n" + + " items: [item],\n" + + " listName\n" + + " }\n" + + " };\n\n" + + "The action output must use multi-line format with proper indentation as shown above.\n" + + "The file must start with a copyright header comment and end with:\n" + " import { ActionType } from \"./schemaFile.ts\";\n" + " : ActionType = | | ...;\n\n" + "Respond in JSON format. Return a JSON object with a single `grammar` key containing the .agr file content as a string.", diff --git a/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts b/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts index 6cae8114ba..25ff0784fb 100644 --- a/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts +++ b/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts @@ -19,13 +19,27 @@ import { updatePhase, writeArtifact, readArtifact, + readArtifactJson, } from "../lib/workspace.js"; import fs from "fs/promises"; import path from "path"; +import { fileURLToPath } from "url"; + +// Sub-schema group type matching discovery/sub-schema-groups.json +type SubSchemaGroup = { + name: string; + description: string; + actions: string[]; +}; + +type SubSchemaSuggestion = { + recommended: boolean; + groups: SubSchemaGroup[]; +}; // Default output root within the TypeAgent repo const AGENTS_DIR = path.resolve( - new URL(import.meta.url).pathname, + fileURLToPath(import.meta.url), "../../../../../../packages/agents", ); @@ -76,7 +90,19 @@ async function handleScaffoldAgent( await fs.mkdir(srcDir, { recursive: true }); - // Write schema and grammar + // Check if sub-schema groups exist from the discovery phase + const subSchemaSuggestion = + await readArtifactJson( + integrationName, + "discovery", + "sub-schema-groups.json", + ); + const subGroups = + subSchemaSuggestion?.recommended && subSchemaSuggestion.groups.length > 0 + ? subSchemaSuggestion.groups + : undefined; + + // Write core schema and grammar await writeFile( path.join(srcDir, `${integrationName}Schema.ts`), schemaTs, @@ -89,27 +115,83 @@ async function handleScaffoldAgent( ), ); - // Stamp out manifest + // Track all files created for the output summary + const files: string[] = [ + `src/${integrationName}Schema.ts`, + `src/${integrationName}Schema.agr`, + ]; + + // If sub-schema groups exist, generate per-group schema and grammar files + if (subGroups) { + const actionsDir = path.join(srcDir, "actions"); + await fs.mkdir(actionsDir, { recursive: true }); + + for (const group of subGroups) { + const groupPascal = toPascalCase(group.name); + + // Generate a filtered schema file for this group + const groupSchemaContent = buildSubSchemaTs( + integrationName, + pascalName, + group, + groupPascal, + schemaTs, + ); + await writeFile( + path.join(actionsDir, `${group.name}ActionsSchema.ts`), + groupSchemaContent, + ); + files.push(`src/actions/${group.name}ActionsSchema.ts`); + + // Generate a filtered grammar file for this group + const groupGrammarContent = buildSubSchemaAgr( + integrationName, + group, + groupPascal, + grammarAgr, + ); + await writeFile( + path.join(actionsDir, `${group.name}ActionsSchema.agr`), + groupGrammarContent, + ); + files.push(`src/actions/${group.name}ActionsSchema.agr`); + } + } + + // Stamp out manifest (with sub-action manifests if groups exist) await writeFile( path.join(srcDir, `${integrationName}Manifest.json`), JSON.stringify( - buildManifest(integrationName, pascalName, state.config.description ?? ""), + buildManifest( + integrationName, + pascalName, + state.config.description ?? "", + subGroups, + ), null, 2, ), ); + files.push(`src/${integrationName}Manifest.json`); // Stamp out handler await writeFile( path.join(srcDir, `${integrationName}ActionHandler.ts`), buildHandler(integrationName, pascalName), ); + files.push(`src/${integrationName}ActionHandler.ts`); - // Stamp out package.json + // Stamp out package.json (with sub-schema build scripts if groups exist) + const subSchemaNames = subGroups?.map((g) => g.name); await writeFile( path.join(targetDir, "package.json"), - JSON.stringify(buildPackageJson(integrationName, packageName, pascalName), null, 2), + JSON.stringify( + buildPackageJson(integrationName, packageName, pascalName, subSchemaNames), + null, + 2, + ), ); + files.push(`package.json`); // Stamp out tsconfigs await writeFile( @@ -120,6 +202,7 @@ async function handleScaffoldAgent( path.join(srcDir, "tsconfig.json"), JSON.stringify(SRC_TSCONFIG, null, 2), ); + files.push(`tsconfig.json`, `src/tsconfig.json`); // Also copy to workspace scaffolder dir for reference await writeArtifact( @@ -131,25 +214,119 @@ async function handleScaffoldAgent( await updatePhase(integrationName, "scaffolder", { status: "approved" }); - const files = [ - `src/${integrationName}Schema.ts`, - `src/${integrationName}Schema.agr`, - `src/${integrationName}Manifest.json`, - `src/${integrationName}ActionHandler.ts`, - `package.json`, - `tsconfig.json`, - `src/tsconfig.json`, - ]; + let subSchemaNote = ""; + if (subGroups) { + subSchemaNote = + `\n\n**Sub-schemas generated:** ${subGroups.length} groups\n` + + subGroups + .map( + (g) => + `- **${g.name}** (${g.actions.length} actions): ${g.description}`, + ) + .join("\n"); + } return createActionResultFromMarkdownDisplay( `## Agent scaffolded: ${integrationName}\n\n` + `**Output directory:** \`${targetDir}\`\n\n` + `**Files created:**\n` + files.map((f) => `- \`${f}\``).join("\n") + + subSchemaNote + `\n\n**Next step:** Phase 6 — use \`generateTests\` and \`runTests\` to validate.`, ); } +// Build a sub-schema TypeScript file that re-exports only the actions belonging +// to this group. It imports from the main schema and creates a union type. +function buildSubSchemaTs( + _integrationName: string, + _pascalName: string, + group: SubSchemaGroup, + groupPascal: string, + fullSchemaTs: string, +): string { + // Extract individual action type names from the full schema that match the + // group's action list. TypeAgent schema files define types like: + // export type BoldAction = { actionName: "bold"; parameters: {...} }; + // and then a union: + // export type FooActions = BoldAction | ItalicAction | ...; + // + // We emit a new file that re-exports only the relevant action types and + // creates a new union type for this sub-schema group. + + const actionTypeNames = group.actions.map( + (a) => `${a.charAt(0).toUpperCase()}${a.slice(1)}Action`, + ); + + // Find action type blocks in the full schema that belong to this group + const actionBlocks: string[] = []; + for (const actionName of group.actions) { + // Match "export type XxxAction = ..." blocks + const typeName = `${actionName.charAt(0).toUpperCase()}${actionName.slice(1)}Action`; + const regex = new RegExp( + `(export\\s+type\\s+${typeName}\\s*=\\s*\\{[\\s\\S]*?\\};)`, + ); + const match = fullSchemaTs.match(regex); + if (match) { + actionBlocks.push(match[1]); + } + } + + const unionType = `export type ${groupPascal}Actions =\n | ${actionTypeNames.join("\n | ")};`; + + return `// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\n// Sub-schema: ${group.name} — ${group.description}\n// Auto-generated by the onboarding scaffolder.\n\n${actionBlocks.join("\n\n")}\n\n${unionType}\n`; +} + +// Build a sub-schema grammar file that includes only the rules relevant to +// this group's actions. +function buildSubSchemaAgr( + integrationName: string, + group: SubSchemaGroup, + groupPascal: string, + fullGrammarAgr: string, +): string { + // Grammar files contain rule blocks that typically start with the action name. + // We extract lines that reference actions in this group and build a new .agr. + const lines = fullGrammarAgr.split("\n"); + const relevantLines: string[] = []; + let inRelevantBlock = false; + const actionSet = new Set(group.actions); + + for (const line of lines) { + // Check if line starts a new action rule (e.g., "actionName:" or + // a line that contains an action name as an identifier) + const ruleMatch = line.match(/^(\w+)\s*:/); + if (ruleMatch) { + inRelevantBlock = actionSet.has(ruleMatch[1]); + } + + // Also include header/import lines (lines starting with '#' or 'from') + const isHeader = + line.startsWith("#") || + line.startsWith("from ") || + line.startsWith("//") || + line.trim() === ""; + + if (inRelevantBlock || isHeader) { + relevantLines.push(line); + } + } + + // Fix the schema file reference to point to the sub-schema + let content = relevantLines.join("\n"); + content = content.replace( + /from "\.\/[^"]*Schema\.ts"/g, + `from "./actions/${group.name}ActionsSchema.ts"`, + ); + // Update the schema type reference + content = content.replace( + /from "\.\/[^"]*"/g, + `from "./actions/${group.name}ActionsSchema.ts"`, + ); + + return `// Sub-schema grammar: ${group.name} — ${group.description}\n// Auto-generated by the onboarding scaffolder.\n\n${content}\n`; +} + async function handleScaffoldPlugin( integrationName: string, template: string, @@ -209,8 +386,13 @@ function toPascalCase(str: string): string { .join(""); } -function buildManifest(name: string, pascalName: string, description: string) { - return { +function buildManifest( + name: string, + pascalName: string, + description: string, + subGroups?: SubSchemaGroup[], +) { + const manifest: Record = { emojiChar: "🔌", description: description || `Agent for ${name}`, defaultEnabled: false, @@ -222,6 +404,25 @@ function buildManifest(name: string, pascalName: string, description: string) { schemaType: `${pascalName}Actions`, }, }; + + if (subGroups && subGroups.length > 0) { + const subActionManifests: Record = {}; + for (const group of subGroups) { + const groupPascal = toPascalCase(group.name); + subActionManifests[group.name] = { + schema: { + description: group.description, + originalSchemaFile: `./actions/${group.name}ActionsSchema.ts`, + schemaFile: `../dist/actions/${group.name}ActionsSchema.pas.json`, + grammarFile: `../dist/actions/${group.name}ActionsSchema.ag.json`, + schemaType: `${groupPascal}Actions`, + }, + }; + } + manifest.subActionManifests = subActionManifests; + } + + return manifest; } function buildHandler(name: string, pascalName: string): string { @@ -260,7 +461,34 @@ async function executeAction( `; } -function buildPackageJson(name: string, packageName: string, pascalName: string) { +function buildPackageJson( + name: string, + packageName: string, + pascalName: string, + subSchemaNames?: string[], +) { + const scripts: Record = { + asc: `asc -i ./src/${name}Schema.ts -o ./dist/${name}Schema.pas.json -t ${pascalName}Actions`, + agc: `agc -i ./src/${name}Schema.agr -o ./dist/${name}Schema.ag.json`, + tsc: "tsc -b", + clean: "rimraf --glob dist *.tsbuildinfo *.done.build.log", + }; + + // Generate asc: and agc: scripts for each sub-schema + const buildTargets = ["npm:tsc", "npm:asc", "npm:agc"]; + if (subSchemaNames && subSchemaNames.length > 0) { + for (const groupName of subSchemaNames) { + const groupPascal = toPascalCase(groupName); + scripts[`asc:${groupName}`] = + `asc -i ./src/actions/${groupName}ActionsSchema.ts -o ./dist/actions/${groupName}ActionsSchema.pas.json -t ${groupPascal}Actions`; + scripts[`agc:${groupName}`] = + `agc -i ./src/actions/${groupName}ActionsSchema.agr -o ./dist/actions/${groupName}ActionsSchema.ag.json`; + buildTargets.push(`npm:asc:${groupName}`, `npm:agc:${groupName}`); + } + } + + scripts.build = `concurrently ${buildTargets.join(" ")}`; + return { name: packageName, version: "0.0.1", @@ -273,13 +501,7 @@ function buildPackageJson(name: string, packageName: string, pascalName: string) "./agent/manifest": `./src/${name}Manifest.json`, "./agent/handlers": `./dist/${name}ActionHandler.js`, }, - scripts: { - asc: `asc -i ./src/${name}Schema.ts -o ./dist/${name}Schema.pas.json -t ${pascalName}Actions`, - agc: `agc -i ./src/${name}Schema.agr -o ./dist/${name}Schema.ag.json`, - build: "concurrently npm:tsc npm:asc npm:agc", - clean: "rimraf --glob dist *.tsbuildinfo *.done.build.log", - tsc: "tsc -b", - }, + scripts, dependencies: { "@typeagent/agent-sdk": "workspace:*", }, diff --git a/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts b/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts index de2c26b565..3e36aeedfa 100644 --- a/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts +++ b/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts @@ -163,14 +163,15 @@ function buildSchemaPrompt( content: "You are a TypeScript expert generating TypeAgent action schemas. " + "TypeAgent action schemas are TypeScript union types where each member has an `actionName` discriminant and a `parameters` object. " + - "Add JSDoc comments to each parameter explaining its purpose and valid values. " + + "IMPORTANT: Use single-line `// comment` syntax for ALL comments. Do NOT use multi-line `/* */` or JSDoc `/** */` comments — the TypeAgent schema parser does not support them. " + + "Add `// comment` lines above each parameter explaining its purpose and valid values. " + "Follow these conventions:\n" + + "- Start the file with:\n // Copyright (c) Microsoft Corporation.\n // Licensed under the MIT License.\n" + "- Export a top-level union type named `Actions`\n" + "- Each action type is named `Action`\n" + "- Use `actionName: \"camelCaseName\"` as a string literal type\n" + "- Parameters use camelCase names\n" + "- Optional parameters use `?: type` syntax\n" + - "- Include the copyright header\n" + "Respond in JSON format. Return a JSON object with a single `schema` key containing the TypeScript file content as a string.", }, { From 06d63b9fe1433ef2b14bf7b277bcf96b244a9aac Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Tue, 7 Apr 2026 23:14:52 -0700 Subject: [PATCH 14/33] added demo script creation action --- .../onboarding/src/onboardingActionHandler.ts | 2 +- .../src/packaging/packagingHandler.ts | 213 ++++++++++++++++++ .../src/packaging/packagingSchema.agr | 16 +- .../src/packaging/packagingSchema.ts | 14 +- 4 files changed, 242 insertions(+), 3 deletions(-) diff --git a/ts/packages/agents/onboarding/src/onboardingActionHandler.ts b/ts/packages/agents/onboarding/src/onboardingActionHandler.ts index a746f228e1..6ede05519f 100644 --- a/ts/packages/agents/onboarding/src/onboardingActionHandler.ts +++ b/ts/packages/agents/onboarding/src/onboardingActionHandler.ts @@ -144,7 +144,7 @@ async function executeAction( } // Packaging phase - if (actionName === "packageAgent" || actionName === "validatePackage") { + if (actionName === "packageAgent" || actionName === "validatePackage" || actionName === "generateDemo") { return executePackagingAction( action as TypeAgentAction, context, diff --git a/ts/packages/agents/onboarding/src/packaging/packagingHandler.ts b/ts/packages/agents/onboarding/src/packaging/packagingHandler.ts index 9a848e6ea8..4298c774f2 100644 --- a/ts/packages/agents/onboarding/src/packaging/packagingHandler.ts +++ b/ts/packages/agents/onboarding/src/packaging/packagingHandler.ts @@ -18,7 +18,10 @@ import { loadState, updatePhase, readArtifact, + readArtifactJson, + writeArtifact, } from "../lib/workspace.js"; +import { getPackagingModel } from "../lib/llm.js"; import { spawn } from "child_process"; import path from "path"; import fs from "fs/promises"; @@ -35,6 +38,11 @@ export async function executePackagingAction( ); case "validatePackage": return handleValidatePackage(action.parameters.integrationName); + case "generateDemo": + return handleGenerateDemo( + action.parameters.integrationName, + action.parameters.durationMinutes, + ); } } @@ -182,6 +190,211 @@ async function handleValidatePackage(integrationName: string): Promise { + const state = await loadState(integrationName); + if (!state) return { error: `Integration "${integrationName}" not found.` }; + if (state.phases.testing.status !== "approved") { + return { error: `Testing phase must be approved before generating a demo.` }; + } + + // Load discovery artifacts + const apiSurface = await readArtifactJson<{ + actions: { name: string; description: string; category?: string }[]; + }>(integrationName, "discovery", "api-surface.json"); + if (!apiSurface) { + return { error: `No approved API surface found. Complete discovery first.` }; + } + + const subSchemaGroups = await readArtifactJson< + Record + >(integrationName, "discovery", "sub-schema-groups.json"); + + // Load the generated schema + const schemaTs = await readArtifact( + integrationName, + "schemaGen", + "schema.ts", + ); + + const duration = durationMinutes ?? "3-5"; + const description = state.config.description ?? integrationName; + + // Build action listing — grouped by sub-schema if available + let actionListing: string; + if (subSchemaGroups) { + const groupLines: string[] = []; + for (const [group, actionNames] of Object.entries(subSchemaGroups)) { + groupLines.push(`### ${group}`); + for (const actionName of actionNames) { + const action = apiSurface.actions.find( + (a) => a.name === actionName, + ); + groupLines.push( + `- **${actionName}**: ${action?.description ?? "(no description)"}`, + ); + } + groupLines.push(""); + } + actionListing = groupLines.join("\n"); + } else { + actionListing = apiSurface.actions + .map((a) => `- **${a.name}**: ${a.description}`) + .join("\n"); + } + + const model = getPackagingModel(); + const prompt = buildDemoPrompt( + integrationName, + description, + actionListing, + schemaTs ?? "", + duration, + ); + + const result = await model.complete(prompt); + if (!result.success) { + return { error: `Demo generation failed: ${result.message}` }; + } + + // Parse the LLM response — expect two fenced blocks: + // ```demo ... ``` and ```narration ... ``` + const responseText = result.data; + const demoScript = extractFencedBlock(responseText, "demo") ?? + extractFirstFencedBlock(responseText) ?? responseText; + const narrationScript = extractFencedBlock(responseText, "narration") ?? + extractSecondFencedBlock(responseText) ?? ""; + + // Find the scaffolded agent directory + const scaffoldedTo = await readArtifact( + integrationName, + "scaffolder", + "scaffolded-to.txt", + ); + + // Write to the shell demo directory alongside other demo scripts + const shellDemoDir = path.resolve( + scaffoldedTo?.trim() ?? ".", + "../../shell/demo", + ); + await fs.mkdir(shellDemoDir, { recursive: true }); + + const demoFilename = `${integrationName}_agent.txt`; + const narrationFilename = `${integrationName}_agent_narration.md`; + + const demoPath = path.join(shellDemoDir, demoFilename); + const narrationPath = path.join(shellDemoDir, narrationFilename); + + await fs.writeFile(demoPath, demoScript, "utf-8"); + await fs.writeFile(narrationPath, narrationScript, "utf-8"); + + // Also save as artifacts in the onboarding workspace + await writeArtifact(integrationName, "packaging", demoFilename, demoScript); + await writeArtifact( + integrationName, + "packaging", + narrationFilename, + narrationScript, + ); + + const lines = [ + `## Demo scripts generated: ${integrationName}`, + ``, + `**Demo script:** \`${demoPath}\``, + `**Narration script:** \`${narrationPath}\``, + ``, + `**Target duration:** ${duration} minutes`, + ``, + `### Demo script preview`, + `\`\`\``, + demoScript.split("\n").slice(0, 20).join("\n"), + demoScript.split("\n").length > 20 ? "..." : "", + `\`\`\``, + ``, + `### Narration preview`, + narrationScript.split("\n").slice(0, 15).join("\n"), + narrationScript.split("\n").length > 15 ? "\n..." : "", + ]; + + return createActionResultFromMarkdownDisplay(lines.join("\n")); +} + +function buildDemoPrompt( + integrationName: string, + description: string, + actionListing: string, + schemaTs: string, + duration: string, +): string { + return `You are generating a demo script for a TypeAgent integration called "${integrationName}". + +**Integration description:** ${description} + +**Available actions (grouped by category if applicable):** +${actionListing} + +${schemaTs ? `**TypeScript action schema:**\n\`\`\`typescript\n${schemaTs}\n\`\`\`` : ""} + +Generate TWO outputs: + +## 1. Demo script (shell format) + +Create a demo script with 5-8 acts that showcase each action category. The demo should be ${duration} minutes long (approximately 50-80 natural language commands). + +Format rules: +- One natural language command per line (what a user would type, NOT @action syntax) +- Use \`# Section Title\` comments for section headers +- Use \`@pauseForInput\` between acts/sections +- Commands should be realistic, conversational requests a user would make +- Progress from simple to complex usage +- Show off different capabilities in each act +- Include some multi-step scenarios + +Wrap the entire demo script in a fenced code block with the label \`demo\`: +\`\`\`demo +# Act 1: Getting Started +... +\`\`\` + +## 2. Narration script (markdown) + +Create a matching narration script with timestamped sections that correspond to each act. Include: +- Approximate timestamp for each section (e.g., [0:00], [0:30]) +- Voice-over text explaining what is being demonstrated +- Key talking points for each act +- Transition text between acts + +Wrap the narration in a fenced code block with the label \`narration\`: +\`\`\`narration +# Demo Narration: ${integrationName} Agent +... +\`\`\``; +} + +function extractFencedBlock(text: string, label: string): string | undefined { + const regex = new RegExp( + "```" + label + "\\s*\\n([\\s\\S]*?)\\n```", + "i", + ); + const match = text.match(regex); + return match?.[1]?.trim(); +} + +function extractFirstFencedBlock(text: string): string | undefined { + const match = text.match(/```[\w]*\s*\n([\s\S]*?)\n```/); + return match?.[1]?.trim(); +} + +function extractSecondFencedBlock(text: string): string | undefined { + const blocks = [...text.matchAll(/```[\w]*\s*\n([\s\S]*?)\n```/g)]; + if (blocks.length >= 2) { + return blocks[1][1]?.trim(); + } + return undefined; +} + async function registerWithDispatcher( integrationName: string, agentDir: string, diff --git a/ts/packages/agents/onboarding/src/packaging/packagingSchema.agr b/ts/packages/agents/onboarding/src/packaging/packagingSchema.agr index f52b8cc9c3..2dca0bee18 100644 --- a/ts/packages/agents/onboarding/src/packaging/packagingSchema.agr +++ b/ts/packages/agents/onboarding/src/packaging/packagingSchema.agr @@ -29,7 +29,21 @@ } }; + = generate (a)? demo (script)? for $(integrationName:wildcard) -> { + actionName: "generateDemo", + parameters: { + integrationName + } +} + | create (a)? demo for $(integrationName:wildcard) -> { + actionName: "generateDemo", + parameters: { + integrationName + } +}; + import { PackagingActions } from "./packagingSchema.ts"; : PackagingActions = - | ; + | + | ; diff --git a/ts/packages/agents/onboarding/src/packaging/packagingSchema.ts b/ts/packages/agents/onboarding/src/packaging/packagingSchema.ts index c597bb8caf..ef29d5ba0f 100644 --- a/ts/packages/agents/onboarding/src/packaging/packagingSchema.ts +++ b/ts/packages/agents/onboarding/src/packaging/packagingSchema.ts @@ -3,7 +3,8 @@ export type PackagingActions = | PackageAgentAction - | ValidatePackageAction; + | ValidatePackageAction + | GenerateDemoAction; export type PackageAgentAction = { actionName: "packageAgent"; @@ -22,3 +23,14 @@ export type ValidatePackageAction = { integrationName: string; }; }; + +// Generates a demo script and narration for the onboarded agent +export type GenerateDemoAction = { + actionName: "generateDemo"; + parameters: { + // Name of the integration + integrationName: string; + // Duration target in minutes (default: 3-5) + durationMinutes?: string; + }; +}; From 1b9922c836a8a0c7cd94174708ee5d00d2455bd4 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Tue, 7 Apr 2026 23:46:37 -0700 Subject: [PATCH 15/33] onboarding agent generates readme and demo scripts --- .../onboarding/src/onboardingActionHandler.ts | 2 +- .../src/packaging/packagingHandler.ts | 113 ++++++++++++++++++ .../src/packaging/packagingSchema.agr | 16 ++- .../src/packaging/packagingSchema.ts | 12 +- 4 files changed, 140 insertions(+), 3 deletions(-) diff --git a/ts/packages/agents/onboarding/src/onboardingActionHandler.ts b/ts/packages/agents/onboarding/src/onboardingActionHandler.ts index 6ede05519f..9d6e1bf4a5 100644 --- a/ts/packages/agents/onboarding/src/onboardingActionHandler.ts +++ b/ts/packages/agents/onboarding/src/onboardingActionHandler.ts @@ -144,7 +144,7 @@ async function executeAction( } // Packaging phase - if (actionName === "packageAgent" || actionName === "validatePackage" || actionName === "generateDemo") { + if (actionName === "packageAgent" || actionName === "validatePackage" || actionName === "generateDemo" || actionName === "generateReadme") { return executePackagingAction( action as TypeAgentAction, context, diff --git a/ts/packages/agents/onboarding/src/packaging/packagingHandler.ts b/ts/packages/agents/onboarding/src/packaging/packagingHandler.ts index 4298c774f2..cf773609ac 100644 --- a/ts/packages/agents/onboarding/src/packaging/packagingHandler.ts +++ b/ts/packages/agents/onboarding/src/packaging/packagingHandler.ts @@ -43,6 +43,8 @@ export async function executePackagingAction( action.parameters.integrationName, action.parameters.durationMinutes, ); + case "generateReadme": + return handleGenerateReadme(action.parameters.integrationName); } } @@ -451,6 +453,117 @@ async function runCommand( }); } +async function handleGenerateReadme( + integrationName: string, +): Promise { + const state = await loadState(integrationName); + if (!state) return { error: `Integration "${integrationName}" not found.` }; + + // Read artifacts for context + const surface = await readArtifactJson<{ + actions: { name: string; description: string }[]; + }>(integrationName, "discovery", "api-surface.json"); + const subGroups = await readArtifactJson<{ + recommended: boolean; + groups: { name: string; description: string; actions: string[] }[]; + }>(integrationName, "discovery", "sub-schema-groups.json"); + const scaffoldedTo = await readArtifact(integrationName, "scaffolder", "scaffolded-to.txt"); + + const description = state.config.description ?? `Agent for ${integrationName}`; + const totalActions = surface?.actions.length ?? 0; + + // Build action listing for the LLM + let actionListing: string; + if (subGroups?.recommended && subGroups.groups.length > 0) { + actionListing = subGroups.groups + .map( + (g) => + `**${g.name}** (${g.actions.length} actions) — ${g.description}\n` + + g.actions.map((a) => ` - ${a}`).join("\n"), + ) + .join("\n\n"); + } else { + actionListing = + surface?.actions + .map((a) => `- **${a.name}** — ${a.description}`) + .join("\n") ?? "No actions discovered."; + } + + const model = getPackagingModel(); + const prompt = [ + { + role: "system" as const, + content: + "You are a technical writer generating a README.md for a TypeAgent agent package. " + + "Write clear, concise documentation in GitHub-flavored Markdown. " + + "Include: overview, architecture diagram (ASCII), action categories table, " + + "prerequisites, quick start, manual setup, project structure, " + + "API limitations (if any actions report limitations), and troubleshooting. " + + "Respond in JSON format with a single `readme` key containing the full Markdown content.", + }, + { + role: "user" as const, + content: + `Generate a README.md for the "${integrationName}" TypeAgent agent.\n\n` + + `Description: ${description}\n\n` + + `Total actions: ${totalActions}\n\n` + + `Actions:\n${actionListing}\n\n` + + `The agent uses a WebSocket bridge pattern where a Node.js bridge server ` + + `connects to an Office Add-in running inside the application. ` + + `The bridge port is 5680. The add-in dev server runs on port 3003.\n\n` + + `The agent was created using the TypeAgent onboarding pipeline.\n\n` + + (subGroups?.recommended + ? `The agent uses ${subGroups.groups.length} sub-schemas: ${subGroups.groups.map((g) => g.name).join(", ")}.\n\n` + : "") + + `Include a quick start section that references:\n` + + ` pnpm run build packages/agents/${integrationName}\n` + + ` npx office-addin-dev-certs install\n` + + ` pnpm run ${integrationName}:addin\n`, + }, + ]; + + const result = await model.complete(prompt); + if (!result.success) { + return { error: `README generation failed: ${result.message}` }; + } + + // Extract README content + let readmeContent: string; + try { + const parsed = JSON.parse(result.data); + readmeContent = parsed.readme ?? result.data; + } catch { + readmeContent = result.data; + } + + // Write to the agent directory + const agentDir = scaffoldedTo?.trim(); + if (agentDir) { + try { + await fs.writeFile( + path.join(agentDir, "README.md"), + readmeContent, + "utf-8", + ); + } catch { + // Fall through — still save as artifact + } + } + + // Save as artifact + await writeArtifact(integrationName, "packaging", "README.md", readmeContent); + + return createActionResultFromMarkdownDisplay( + `## README generated: ${integrationName}\n\n` + + (agentDir + ? `Written to \`${path.join(agentDir, "README.md")}\`\n\n` + : "") + + `**Preview (first 2000 chars):**\n\n` + + readmeContent.slice(0, 2000) + + (readmeContent.length > 2000 ? "\n\n_...truncated_" : ""), + ); +} + async function fileExists(p: string): Promise { try { await fs.access(p); diff --git a/ts/packages/agents/onboarding/src/packaging/packagingSchema.agr b/ts/packages/agents/onboarding/src/packaging/packagingSchema.agr index 2dca0bee18..a306643cc6 100644 --- a/ts/packages/agents/onboarding/src/packaging/packagingSchema.agr +++ b/ts/packages/agents/onboarding/src/packaging/packagingSchema.agr @@ -42,8 +42,22 @@ } }; + = generate (a)? readme for $(integrationName:wildcard) -> { + actionName: "generateReadme", + parameters: { + integrationName + } +} + | create (a)? readme for (the)? $(integrationName:wildcard) (agent)? -> { + actionName: "generateReadme", + parameters: { + integrationName + } +}; + import { PackagingActions } from "./packagingSchema.ts"; : PackagingActions = | - | ; + | + | ; diff --git a/ts/packages/agents/onboarding/src/packaging/packagingSchema.ts b/ts/packages/agents/onboarding/src/packaging/packagingSchema.ts index ef29d5ba0f..de3a68ae04 100644 --- a/ts/packages/agents/onboarding/src/packaging/packagingSchema.ts +++ b/ts/packages/agents/onboarding/src/packaging/packagingSchema.ts @@ -4,7 +4,8 @@ export type PackagingActions = | PackageAgentAction | ValidatePackageAction - | GenerateDemoAction; + | GenerateDemoAction + | GenerateReadmeAction; export type PackageAgentAction = { actionName: "packageAgent"; @@ -24,6 +25,15 @@ export type ValidatePackageAction = { }; }; +// Generates a README.md for the onboarded agent +export type GenerateReadmeAction = { + actionName: "generateReadme"; + parameters: { + // Name of the integration + integrationName: string; + }; +}; + // Generates a demo script and narration for the onboarded agent export type GenerateDemoAction = { actionName: "generateDemo"; From 9de7e5e4171f930f0106be3bfc9fd2d72616bc69 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Wed, 8 Apr 2026 14:06:04 -0700 Subject: [PATCH 16/33] Updated onboarding agent with fixes based on using the onboarding agent. --- .../src/discovery/discoveryHandler.ts | 109 ++++++++++++++++-- .../src/schemaGen/schemaGenHandler.ts | 17 +-- 2 files changed, 111 insertions(+), 15 deletions(-) diff --git a/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts b/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts index 3a4564801f..54e6acbe60 100644 --- a/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts +++ b/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts @@ -111,21 +111,29 @@ async function handleCrawlDocUrl( return { error: `Failed to fetch ${url}: ${err?.message ?? err}` }; } + // Strip HTML tags and collapse whitespace to get readable text content + const textContent = stripHtml(pageContent); + + // Follow links up to maxDepth levels + const linkedContent = await crawlLinks(url, pageContent, maxDepth, integrationName); + // Use LLM to extract API actions from the page content const prompt = [ { role: "system" as const, content: - "You are an API documentation analyzer. Extract a list of API actions/operations from the provided documentation HTML. " + + "You are an API documentation analyzer. Extract a list of user-facing API actions/operations from the provided documentation. " + "For each action, identify: name (camelCase), description, HTTP method (if applicable), endpoint path (if applicable), and parameters. " + - "Return a JSON array of actions.", + "IMPORTANT: Only include actions that represent real operations a user would invoke. " + + "Exclude internal/infrastructure methods like: load, sync, toJSON, context, track, untrack, set, get (bare getters/setters without a domain concept). " + + "Return a JSON array of actions with shape: { name, description, method?, path?, parameters?: [{name, type, description?, required?}] }[]", }, { role: "user" as const, content: - `Extract all API actions from this documentation page for the "${integrationName}" integration.\n\n` + - `URL: ${url}\n\n` + - `Content (truncated to 8000 chars):\n${pageContent.slice(0, 8000)}`, + `Extract all user-facing API actions from this documentation for the "${integrationName}" integration.\n\n` + + `Primary URL: ${url}\n\n` + + `Content:\n${(textContent + "\n\n" + linkedContent).slice(0, 16000)}`, }, ]; @@ -145,8 +153,10 @@ async function handleCrawlDocUrl( return { error: "Failed to parse LLM response as JSON action list." }; } - // Add source URL to each action - actions = actions.map((a) => ({ ...a, sourceUrl: url })); + // Add source URL to each action; filter out internal framework methods + actions = actions + .map((a) => ({ ...a, sourceUrl: url })) + .filter((a) => !isInternalAction(a.name)); // Merge with any existing discovered actions const existing = await readArtifactJson( @@ -188,6 +198,91 @@ async function handleCrawlDocUrl( ); } +// ── HTML helpers ───────────────────────────────────────────────────────────── + +// Strip HTML tags and collapse whitespace to extract readable text. +function stripHtml(html: string): string { + return html + .replace(//gi, "") + .replace(//gi, "") + .replace(/<[^>]+>/g, " ") + .replace(/ /g, " ") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/&/g, "&") + .replace(/"/g, '"') + .replace(/\s{2,}/g, " ") + .trim(); +} + +// Extract same-origin links from an HTML page. +function extractLinks(baseUrl: string, html: string): string[] { + const base = new URL(baseUrl); + const links: string[] = []; + const hrefRe = /href=["']([^"'#?]+)["']/gi; + let m: RegExpExecArray | null; + while ((m = hrefRe.exec(html)) !== null) { + try { + const resolved = new URL(m[1], baseUrl); + // Only follow links on the same hostname and path prefix + if ( + resolved.hostname === base.hostname && + resolved.pathname.startsWith(base.pathname.split("/").slice(0, -1).join("/")) + ) { + links.push(resolved.href); + } + } catch { + // skip malformed URLs + } + } + // Deduplicate + return [...new Set(links)].slice(0, 30); // cap at 30 links +} + +// Crawl linked pages up to maxDepth and return combined text (capped to 8000 chars per page). +async function crawlLinks( + baseUrl: string, + baseHtml: string, + maxDepth: number, + _integrationName: string, +): Promise { + if (maxDepth <= 1) return ""; + + const links = extractLinks(baseUrl, baseHtml); + const visited = new Set([baseUrl]); + const chunks: string[] = []; + + for (const link of links.slice(0, 15)) { + if (visited.has(link)) continue; + visited.add(link); + try { + const resp = await fetch(link); + if (!resp.ok) continue; + const html = await resp.text(); + const text = stripHtml(html).slice(0, 8000); + chunks.push(`\n--- ${link} ---\n${text}`); + } catch { + // skip unreachable pages + } + } + + return chunks.join("\n").slice(0, 40000); +} + +// Names that are internal Office.js / API framework infrastructure, not user-facing operations. +const INTERNAL_ACTION_NAMES = new Set([ + "load", "sync", "toJSON", "track", "untrack", "context", + "getItem", "getCount", "getItemOrNullObject", "getFirstOrNullObject", + "getLastOrNullObject", "getLast", "getFirst", "items", +]); + +function isInternalAction(name: string): boolean { + if (INTERNAL_ACTION_NAMES.has(name)) return true; + // Bare getters/setters with no domain concept (e.g. "get", "set", "load") + if (/^(get|set|load|read|fetch)$/.test(name)) return true; + return false; +} + async function handleParseOpenApiSpec( integrationName: string, specSource: string, diff --git a/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts b/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts index 238e2f0c62..8fc428c1f6 100644 --- a/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts +++ b/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts @@ -47,20 +47,21 @@ export async function executeSchemaGenAction( async function handleGenerateSchema(integrationName: string): Promise { const state = await loadState(integrationName); if (!state) return { error: `Integration "${integrationName}" not found.` }; - if (state.phases.phraseGen.status !== "approved") { - return { error: `Phrase generation phase must be approved first. Run approvePhrases.` }; + if (state.phases.discovery.status !== "approved") { + return { error: `Discovery phase must be approved first. Run approveApiSurface.` }; } const surface = await readArtifactJson(integrationName, "discovery", "api-surface.json"); - const phraseSet = await readArtifactJson(integrationName, "phraseGen", "phrases.json"); - if (!surface || !phraseSet) { - return { error: `Missing discovery or phrase artifacts for "${integrationName}".` }; + if (!surface) { + return { error: `Missing discovery artifact for "${integrationName}".` }; } + // phraseSet is optional — we can still generate a schema without sample phrases + const phraseSet = await readArtifactJson(integrationName, "phraseGen", "phrases.json"); await updatePhase(integrationName, "schemaGen", { status: "in-progress" }); const model = getSchemaGenModel(); - const prompt = buildSchemaPrompt(integrationName, surface, phraseSet, state.config.description); + const prompt = buildSchemaPrompt(integrationName, surface, phraseSet ?? null, state.config.description); const result = await model.complete(prompt); if (!result.success) { return { error: `Schema generation failed: ${result.message}` }; @@ -138,12 +139,12 @@ async function handleApproveSchema(integrationName: string): Promise { - const phrases = phraseSet.phrases[a.name] ?? []; + const phrases = phraseSet?.phrases[a.name] ?? []; return ( `Action: ${a.name}\n` + `Description: ${a.description}\n` + From 43898088d256a77486f05b0f2d77dea1dff63f10 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Sat, 11 Apr 2026 09:31:07 -0700 Subject: [PATCH 17/33] added best practice section with one exmaple --- .../onboarding/src/schemaGen/schemaGenHandler.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts b/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts index 99f6086684..6bf730efdf 100644 --- a/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts +++ b/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts @@ -92,7 +92,14 @@ async function handleRefineSchema( role: "system" as const, content: "You are a TypeScript expert. Modify the given TypeAgent action schema according to the instructions. " + - "Preserve all copyright headers and existing structure. Respond in JSON format. Return a JSON object with a single `schema` key containing the updated TypeScript file content as a string.", + "Preserve all copyright headers and existing structure. " + + "IMPORTANT: Use single-line `// comment` syntax for ALL comments. Do NOT use multi-line `/* */` or JSDoc `/** */` comments — the TypeAgent schema parser does not support them. " + + "Follow these best practices:\n" + + "- Enum-like properties: always define the type as an explicit union of string literals instead of `string`. " + + "The inline comment should name the underlying API enum it maps to and explain the default value and why. " + + "Example: `position?: \"Top\" | \"Bottom\" | \"Center\" | \"InsideEnd\" | \"InsideBase\" | \"OutsideEnd\" | \"Left\" | \"Right\" | \"BestFit\" | \"Callout\" | \"None\"; " + + "// Label position (Office.js ChartDataLabelPosition enum). Default is \"BestFit\" (automatic placement chosen by Office.js)`\n" + + "Respond in JSON format. Return a JSON object with a single `schema` key containing the updated TypeScript file content as a string.", }, { role: "user" as const, @@ -173,6 +180,11 @@ function buildSchemaPrompt( "- Use `actionName: \"camelCaseName\"` as a string literal type\n" + "- Parameters use camelCase names\n" + "- Optional parameters use `?: type` syntax\n" + + "Follow these best practices:\n" + + "- Enum-like properties: always define the type as an explicit union of string literals instead of `string`. " + + "The inline comment should name the underlying API enum it maps to and explain the default value and why. " + + "Example: `position?: \"Top\" | \"Bottom\" | \"Center\" | \"InsideEnd\" | \"InsideBase\" | \"OutsideEnd\" | \"Left\" | \"Right\" | \"BestFit\" | \"Callout\" | \"None\"; " + + "// Label position (Office.js ChartDataLabelPosition enum). Default is \"BestFit\" (automatic placement chosen by Office.js)`\n" + "Respond in JSON format. Return a JSON object with a single `schema` key containing the TypeScript file content as a string.", }, { From 3f0da236145dc193dce993773e7c085c85dbeca0 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Sat, 11 Apr 2026 10:43:39 -0700 Subject: [PATCH 18/33] updated schema guidlines --- .../src/schemaGen/schemaGenHandler.ts | 48 +++++++++++++------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts b/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts index 6bf730efdf..f2a4ecfaaa 100644 --- a/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts +++ b/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts @@ -25,6 +25,35 @@ import { getSchemaGenModel } from "../lib/llm.js"; import { ApiSurface } from "../discovery/discoveryHandler.js"; import { PhraseSet } from "../phraseGen/phraseGenHandler.js"; +// Shared schema authoring guidelines injected into every schema gen/refine prompt. +const SCHEMA_GUIDELINES = ` +COMMENT STRUCTURE RULES: +1. Action-level block (above the action type declaration): use only for a short "what it does" description and example user/agent phrase pairs. No rules or constraints here. +2. Property-level comments (inside the parameters object, above each property declaration): ALL guidance lives here, co-located with the property it constrains. Do NOT put constraints at the action level. +3. No inline end-of-line comments on property declarations. All commentary goes in the line(s) above the property. + +PROPERTY COMMENT ORDERING (top = least important, bottom = most important — the LLM reads top-to-bottom, so put the critical constraint last, immediately before the property): +// General description of what this parameter is. +// Supplementary guidance / common aliases / optional tips. +// NOTE: or IMPORTANT: The hard constraint the model must not violate. +propertyName: type; + +CRITICAL CONSTRAINT FORMAT — embed a concrete WRONG/RIGHT example for any hard constraint; the WRONG case should be the exact failure mode you have observed: +// The data range in A1 notation. +// NOTE: Must be a literal cell range — do NOT use named ranges or structured references. +// WRONG: "SalesData[ActualSales]" ← structured table reference, will fail +// WRONG: "ActualSales" ← column name, will fail +// RIGHT: "C1:C7" ← literal A1 range +dataRange: string; + +BEST PRACTICES: +- Enum-like properties: always define the type as an explicit union of string literals instead of \`string\`. The comment above the property should name the underlying API enum it maps to and explain the default value and why. + Example: + // Label position relative to the data point. Maps to Office.js ChartDataLabelPosition enum. + // Default is "BestFit" — Office.js automatically chooses the best placement. + position?: "Top" | "Bottom" | "Center" | "InsideEnd" | "InsideBase" | "OutsideEnd" | "Left" | "Right" | "BestFit" | "Callout" | "None"; +`; + export async function executeSchemaGenAction( action: TypeAgentAction, _context: ActionContext, @@ -92,13 +121,8 @@ async function handleRefineSchema( role: "system" as const, content: "You are a TypeScript expert. Modify the given TypeAgent action schema according to the instructions. " + - "Preserve all copyright headers and existing structure. " + - "IMPORTANT: Use single-line `// comment` syntax for ALL comments. Do NOT use multi-line `/* */` or JSDoc `/** */` comments — the TypeAgent schema parser does not support them. " + - "Follow these best practices:\n" + - "- Enum-like properties: always define the type as an explicit union of string literals instead of `string`. " + - "The inline comment should name the underlying API enum it maps to and explain the default value and why. " + - "Example: `position?: \"Top\" | \"Bottom\" | \"Center\" | \"InsideEnd\" | \"InsideBase\" | \"OutsideEnd\" | \"Left\" | \"Right\" | \"BestFit\" | \"Callout\" | \"None\"; " + - "// Label position (Office.js ChartDataLabelPosition enum). Default is \"BestFit\" (automatic placement chosen by Office.js)`\n" + + "Preserve all copyright headers and existing structure.\n" + + SCHEMA_GUIDELINES + "Respond in JSON format. Return a JSON object with a single `schema` key containing the updated TypeScript file content as a string.", }, { @@ -171,20 +195,14 @@ function buildSchemaPrompt( content: "You are a TypeScript expert generating TypeAgent action schemas. " + "TypeAgent action schemas are TypeScript union types where each member has an `actionName` discriminant and a `parameters` object. " + - "IMPORTANT: Use single-line `// comment` syntax for ALL comments. Do NOT use multi-line `/* */` or JSDoc `/** */` comments — the TypeAgent schema parser does not support them. " + - "Add `// comment` lines above each parameter explaining its purpose and valid values. " + - "Follow these conventions:\n" + + "Follow these file-level conventions:\n" + "- Start the file with:\n // Copyright (c) Microsoft Corporation.\n // Licensed under the MIT License.\n" + "- Export a top-level union type named `Actions`\n" + "- Each action type is named `Action`\n" + "- Use `actionName: \"camelCaseName\"` as a string literal type\n" + "- Parameters use camelCase names\n" + "- Optional parameters use `?: type` syntax\n" + - "Follow these best practices:\n" + - "- Enum-like properties: always define the type as an explicit union of string literals instead of `string`. " + - "The inline comment should name the underlying API enum it maps to and explain the default value and why. " + - "Example: `position?: \"Top\" | \"Bottom\" | \"Center\" | \"InsideEnd\" | \"InsideBase\" | \"OutsideEnd\" | \"Left\" | \"Right\" | \"BestFit\" | \"Callout\" | \"None\"; " + - "// Label position (Office.js ChartDataLabelPosition enum). Default is \"BestFit\" (automatic placement chosen by Office.js)`\n" + + SCHEMA_GUIDELINES + "Respond in JSON format. Return a JSON object with a single `schema` key containing the TypeScript file content as a string.", }, { From 5a8d21c86e84fa7ffe1d1bdb890bd62bb45ef26c Mon Sep 17 00:00:00 2001 From: robgruen Date: Mon, 13 Apr 2026 14:10:46 -0700 Subject: [PATCH 19/33] lint --- .../agentServer/server/src/sessionManager.ts | 4 +- ts/packages/agents/onboarding/README.md | 18 +-- ts/packages/agents/onboarding/USER_GUIDE.md | 53 ++++--- .../src/discovery/discoveryHandler.ts | 143 +++++++++++++----- .../src/grammarGen/grammarGenHandler.ts | 132 ++++++++++++---- .../onboarding/src/onboardingActionHandler.ts | 7 +- .../src/packaging/packagingHandler.ts | 86 +++++++---- .../src/phraseGen/phraseGenHandler.ts | 34 ++++- .../src/scaffolder/scaffolderHandler.ts | 54 ++++--- .../src/scaffolder/scaffolderSchema.ts | 7 +- .../src/schemaGen/schemaGenHandler.ts | 87 ++++++++--- .../onboarding/src/testing/testingHandler.ts | 116 +++++++++++--- .../commandExecutor/src/commandServer.ts | 6 +- 13 files changed, 542 insertions(+), 205 deletions(-) diff --git a/ts/packages/agentServer/server/src/sessionManager.ts b/ts/packages/agentServer/server/src/sessionManager.ts index ef0c32fbbe..f812807997 100644 --- a/ts/packages/agentServer/server/src/sessionManager.ts +++ b/ts/packages/agentServer/server/src/sessionManager.ts @@ -151,7 +151,9 @@ export async function createSessionManager( return path.join(sessionsDir, sessionId); } - function ensureDispatcher(record: SessionRecord): Promise { + function ensureDispatcher( + record: SessionRecord, + ): Promise { if (record.sharedDispatcher !== undefined) { return Promise.resolve(record.sharedDispatcher); } diff --git a/ts/packages/agents/onboarding/README.md b/ts/packages/agents/onboarding/README.md index 97b280cc43..534325fb4e 100644 --- a/ts/packages/agents/onboarding/README.md +++ b/ts/packages/agents/onboarding/README.md @@ -6,15 +6,15 @@ A TypeAgent agent that automates the end-to-end process of integrating a new app Integrating a new application into TypeAgent involves 7 phases: -| Phase | Sub-agent | What it does | -|---|---|---| -| 1 | `onboarding-discovery` | Crawls docs or parses an OpenAPI spec to enumerate the API surface | -| 2 | `onboarding-phrasegen` | Generates natural language sample phrases for each action | -| 3 | `onboarding-schemagen` | Generates TypeScript action schemas from the API surface | -| 4 | `onboarding-grammargen` | Generates `.agr` grammar files from schemas and phrases | -| 5 | `onboarding-scaffolder` | Stamps out the agent package infrastructure | -| 6 | `onboarding-testing` | Generates test cases and runs a phrase→action validation loop | -| 7 | `onboarding-packaging` | Packages the agent for distribution and registration | +| Phase | Sub-agent | What it does | +| ----- | ----------------------- | ------------------------------------------------------------------ | +| 1 | `onboarding-discovery` | Crawls docs or parses an OpenAPI spec to enumerate the API surface | +| 2 | `onboarding-phrasegen` | Generates natural language sample phrases for each action | +| 3 | `onboarding-schemagen` | Generates TypeScript action schemas from the API surface | +| 4 | `onboarding-grammargen` | Generates `.agr` grammar files from schemas and phrases | +| 5 | `onboarding-scaffolder` | Stamps out the agent package infrastructure | +| 6 | `onboarding-testing` | Generates test cases and runs a phrase→action validation loop | +| 7 | `onboarding-packaging` | Packages the agent for distribution and registration | Each phase produces **artifacts saved to disk** at `~/.typeagent/onboarding//`, so work can be resumed across sessions. diff --git a/ts/packages/agents/onboarding/USER_GUIDE.md b/ts/packages/agents/onboarding/USER_GUIDE.md index 441e134b87..f6146e5992 100644 --- a/ts/packages/agents/onboarding/USER_GUIDE.md +++ b/ts/packages/agents/onboarding/USER_GUIDE.md @@ -14,10 +14,10 @@ Before you can use the onboarding agent from your AI assistant, you need to regi TypeAgent exposes a stdio MCP server at `ts/packages/commandExecutor/dist/server.js`. It provides three tools to your AI assistant: -| Tool | What it does | -|---|---| -| `discover_agents` | Lists all TypeAgent agents and their actions | -| `execute_action` | Calls any agent action directly by name with typed parameters | +| Tool | What it does | +| ----------------- | ------------------------------------------------------------- | +| `discover_agents` | Lists all TypeAgent agents and their actions | +| `execute_action` | Calls any agent action directly by name with typed parameters | | `execute_command` | Passes a natural language request to the TypeAgent dispatcher | The onboarding agent's actions (`startOnboarding`, `crawlDocUrl`, `generateSchema`, etc.) are discovered and called via these tools. @@ -52,7 +52,9 @@ You should see `command-executor` listed as connected. "mcpServers": { "typeagent": { "command": "node", - "args": ["/ts/packages/commandExecutor/dist/server.js"], + "args": [ + "/ts/packages/commandExecutor/dist/server.js" + ], "env": {} } } @@ -60,6 +62,7 @@ You should see `command-executor` listed as connected. ``` Replace `` with the full path to your TypeAgent clone, for example: + - Windows: `C:/repos/TypeAgent/ts/packages/commandExecutor/dist/server.js` - Mac/Linux: `/home/you/repos/TypeAgent/ts/packages/commandExecutor/dist/server.js` @@ -78,7 +81,9 @@ GitHub Copilot uses VS Code's MCP configuration. Add the TypeAgent server via th "github.copilot.chat.mcpServers": { "typeagent": { "command": "node", - "args": ["/ts/packages/commandExecutor/dist/server.js"], + "args": [ + "/ts/packages/commandExecutor/dist/server.js" + ], "type": "stdio" } } @@ -86,6 +91,7 @@ GitHub Copilot uses VS Code's MCP configuration. Add the TypeAgent server via th ``` **Via the VS Code UI** — open the Command Palette (`Ctrl+Shift+P`), run **"MCP: Add MCP Server"**, choose **"Command (stdio)"**, and enter: + - Command: `node` - Args: `/ts/packages/commandExecutor/dist/server.js` - Name: `typeagent` @@ -111,6 +117,7 @@ or The assistant will call `discover_agents` and return a list that includes `onboarding` (among others). If you see the list, you're ready to start onboarding. **Troubleshooting:** + - If the server isn't found, check that `ts/packages/commandExecutor/dist/server.js` exists — run `pnpm run build` from `ts/` if not - If tools don't appear, restart your AI assistant or reload the VS Code window - Logs are written to `~/.tmp/typeagent-mcp/` — check there for connection errors @@ -267,6 +274,7 @@ If compilation fails, the error message will tell you which rule is invalid. You ``` > Generate the grammar for slack ``` + again after the schema is adjusted, or manually edit the grammar file at `~/.typeagent/onboarding/slack/grammarGen/schema.agr`. When the grammar compiles cleanly: @@ -284,6 +292,7 @@ When the grammar compiles cleanly: ``` This stamps out a complete TypeAgent agent package at `ts/packages/agents/slack/` with: + - `slackManifest.json` - `slackSchema.ts` (the approved schema) - `slackSchema.agr` (the approved grammar) @@ -415,13 +424,13 @@ After scaffolding, you'll have a stub handler at `ts/packages/agents/slack/src/s ```typescript async function executeAction( - action: TypeAgentAction, - context: ActionContext, + action: TypeAgentAction, + context: ActionContext, ): Promise { - // TODO: implement action handlers - return createActionResultFromTextDisplay( - `Executing ${action.actionName} — not yet implemented.`, - ); + // TODO: implement action handlers + return createActionResultFromTextDisplay( + `Executing ${action.actionName} — not yet implemented.`, + ); } ``` @@ -429,16 +438,16 @@ Fill in the cases using the Slack Web API client. For example: ```typescript switch (action.actionName) { - case "postMessage": { - const result = await slackClient.chat.postMessage({ - channel: action.parameters.channelId, - text: action.parameters.message, - }); - return createActionResultFromTextDisplay( - `Message sent to ${action.parameters.channelId}`, - ); - } - // ... + case "postMessage": { + const result = await slackClient.chat.postMessage({ + channel: action.parameters.channelId, + text: action.parameters.message, + }); + return createActionResultFromTextDisplay( + `Message sent to ${action.parameters.channelId}`, + ); + } + // ... } ``` diff --git a/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts b/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts index 00e427f626..7063be6c72 100644 --- a/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts +++ b/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts @@ -10,9 +10,7 @@ import { TypeAgentAction, ActionResult, } from "@typeagent/agent-sdk"; -import { - createActionResultFromMarkdownDisplay, -} from "@typeagent/agent-sdk/helpers/action"; +import { createActionResultFromMarkdownDisplay } from "@typeagent/agent-sdk/helpers/action"; import { DiscoveryActions } from "./discoverySchema.js"; import { loadState, @@ -92,7 +90,9 @@ async function handleCrawlDocUrl( ): Promise { const state = await loadState(integrationName); if (!state) { - return { error: `Integration "${integrationName}" not found. Run startOnboarding first.` }; + return { + error: `Integration "${integrationName}" not found. Run startOnboarding first.`, + }; } await updatePhase(integrationName, "discovery", { status: "in-progress" }); @@ -104,7 +104,9 @@ async function handleCrawlDocUrl( try { const response = await fetch(url); if (!response.ok) { - return { error: `Failed to fetch ${url}: ${response.status} ${response.statusText}` }; + return { + error: `Failed to fetch ${url}: ${response.status} ${response.statusText}`, + }; } pageContent = await response.text(); } catch (err: any) { @@ -115,7 +117,12 @@ async function handleCrawlDocUrl( const textContent = stripHtml(pageContent); // Follow links up to maxDepth levels - const linkedContent = await crawlLinks(url, pageContent, maxDepth, integrationName); + const linkedContent = await crawlLinks( + url, + pageContent, + maxDepth, + integrationName, + ); // Use LLM to extract API actions from the page content const prompt = [ @@ -169,8 +176,8 @@ async function handleCrawlDocUrl( discoveredAt: new Date().toISOString(), source: url, actions: [ - ...(existing?.actions ?? []).filter((a) => - !actions.find((n) => n.name === a.name), + ...(existing?.actions ?? []).filter( + (a) => !actions.find((n) => n.name === a.name), ), ...actions, ], @@ -227,7 +234,9 @@ function extractLinks(baseUrl: string, html: string): string[] { // Only follow links on the same hostname and path prefix if ( resolved.hostname === base.hostname && - resolved.pathname.startsWith(base.pathname.split("/").slice(0, -1).join("/")) + resolved.pathname.startsWith( + base.pathname.split("/").slice(0, -1).join("/"), + ) ) { links.push(resolved.href); } @@ -271,9 +280,20 @@ async function crawlLinks( // Names that are internal Office.js / API framework infrastructure, not user-facing operations. const INTERNAL_ACTION_NAMES = new Set([ - "load", "sync", "toJSON", "track", "untrack", "context", - "getItem", "getCount", "getItemOrNullObject", "getFirstOrNullObject", - "getLastOrNullObject", "getLast", "getFirst", "items", + "load", + "sync", + "toJSON", + "track", + "untrack", + "context", + "getItem", + "getCount", + "getItemOrNullObject", + "getFirstOrNullObject", + "getLastOrNullObject", + "getLast", + "getFirst", + "items", ]); function isInternalAction(name: string): boolean { @@ -289,7 +309,9 @@ async function handleParseOpenApiSpec( ): Promise { const state = await loadState(integrationName); if (!state) { - return { error: `Integration "${integrationName}" not found. Run startOnboarding first.` }; + return { + error: `Integration "${integrationName}" not found. Run startOnboarding first.`, + }; } await updatePhase(integrationName, "discovery", { status: "in-progress" }); @@ -297,10 +319,15 @@ async function handleParseOpenApiSpec( // Fetch the spec (URL or file path) let specContent: string; try { - if (specSource.startsWith("http://") || specSource.startsWith("https://")) { + if ( + specSource.startsWith("http://") || + specSource.startsWith("https://") + ) { const response = await fetch(specSource); if (!response.ok) { - return { error: `Failed to fetch spec: ${response.status} ${response.statusText}` }; + return { + error: `Failed to fetch spec: ${response.status} ${response.statusText}`, + }; } specContent = await response.text(); } else { @@ -308,7 +335,9 @@ async function handleParseOpenApiSpec( specContent = await fs.readFile(specSource, "utf-8"); } } catch (err: any) { - return { error: `Failed to read spec from ${specSource}: ${err?.message ?? err}` }; + return { + error: `Failed to read spec from ${specSource}: ${err?.message ?? err}`, + }; } let spec: any; @@ -317,7 +346,9 @@ async function handleParseOpenApiSpec( } catch { try { // Try YAML if JSON fails (basic line parsing) - return { error: "YAML specs not yet supported — please provide a JSON OpenAPI spec." }; + return { + error: "YAML specs not yet supported — please provide a JSON OpenAPI spec.", + }; } catch { return { error: "Could not parse spec as JSON or YAML." }; } @@ -326,13 +357,27 @@ async function handleParseOpenApiSpec( // Extract actions from OpenAPI paths const actions: DiscoveredAction[] = []; const paths = spec.paths ?? {}; - for (const [pathStr, pathItem] of Object.entries(paths) as [string, any][]) { - for (const method of ["get", "post", "put", "patch", "delete"] as const) { + for (const [pathStr, pathItem] of Object.entries(paths) as [ + string, + any, + ][]) { + for (const method of [ + "get", + "post", + "put", + "patch", + "delete", + ] as const) { const op = pathItem?.[method]; if (!op) continue; - const name = op.operationId ?? `${method}${pathStr.replace(/[^a-zA-Z0-9]/g, "_")}`; - const camelName = name.replace(/_([a-z])/g, (_: string, c: string) => c.toUpperCase()); + const name = + op.operationId ?? + `${method}${pathStr.replace(/[^a-zA-Z0-9]/g, "_")}`; + const camelName = name.replace( + /_([a-z])/g, + (_: string, c: string) => c.toUpperCase(), + ); const parameters: DiscoveredParameter[] = (op.parameters ?? []).map( (p: any) => ({ @@ -344,21 +389,28 @@ async function handleParseOpenApiSpec( ); // Also include request body fields as parameters - const requestBody = op.requestBody?.content?.["application/json"]?.schema; + const requestBody = + op.requestBody?.content?.["application/json"]?.schema; if (requestBody?.properties) { - for (const [propName, propSchema] of Object.entries(requestBody.properties) as [string, any][]) { + for (const [propName, propSchema] of Object.entries( + requestBody.properties, + ) as [string, any][]) { parameters.push({ name: propName, type: propSchema.type ?? "string", description: propSchema.description, - required: requestBody.required?.includes(propName) ?? false, + required: + requestBody.required?.includes(propName) ?? false, }); } } actions.push({ name: camelName, - description: op.summary ?? op.description ?? `${method.toUpperCase()} ${pathStr}`, + description: + op.summary ?? + op.description ?? + `${method.toUpperCase()} ${pathStr}`, method: method.toUpperCase(), path: pathStr, parameters, @@ -374,7 +426,12 @@ async function handleParseOpenApiSpec( actions, }; - await writeArtifactJson(integrationName, "discovery", "api-surface.json", surface); + await writeArtifactJson( + integrationName, + "discovery", + "api-surface.json", + surface, + ); return createActionResultFromMarkdownDisplay( `## OpenAPI spec parsed: ${integrationName}\n\n` + @@ -383,9 +440,14 @@ async function handleParseOpenApiSpec( `**Actions found:** ${actions.length}\n\n` + actions .slice(0, 20) - .map((a) => `- **${a.name}** (\`${a.method} ${a.path}\`): ${a.description}`) + .map( + (a) => + `- **${a.name}** (\`${a.method} ${a.path}\`): ${a.description}`, + ) .join("\n") + - (actions.length > 20 ? `\n\n_...and ${actions.length - 20} more_` : "") + + (actions.length > 20 + ? `\n\n_...and ${actions.length - 20} more_` + : "") + `\n\nReview with \`listDiscoveredActions\`, then \`approveApiSurface\` to proceed.`, ); } @@ -399,7 +461,9 @@ async function handleListDiscoveredActions( "api-surface.json", ); if (!surface) { - return { error: `No discovered actions found for "${integrationName}". Run crawlDocUrl or parseOpenApiSpec first.` }; + return { + error: `No discovered actions found for "${integrationName}". Run crawlDocUrl or parseOpenApiSpec first.`, + }; } const lines = [ @@ -413,8 +477,7 @@ async function handleListDiscoveredActions( `| # | Name | Description |`, `|---|---|---|`, ...surface.actions.map( - (a, i) => - `| ${i + 1} | \`${a.name}\` | ${a.description} |`, + (a, i) => `| ${i + 1} | \`${a.name}\` | ${a.description} |`, ), ]; @@ -432,7 +495,9 @@ async function handleApproveApiSurface( "api-surface.json", ); if (!surface) { - return { error: `No discovered actions found for "${integrationName}".` }; + return { + error: `No discovered actions found for "${integrationName}".`, + }; } let approved = surface.actions; @@ -451,7 +516,12 @@ async function handleApproveApiSurface( actions: approved, }; - await writeArtifactJson(integrationName, "discovery", "api-surface.json", updated); + await writeArtifactJson( + integrationName, + "discovery", + "api-surface.json", + updated, + ); await updatePhase(integrationName, "discovery", { status: "approved" }); // If many actions, recommend sub-schema categorization @@ -466,7 +536,9 @@ async function handleApproveApiSurface( return createActionResultFromMarkdownDisplay( `## API surface approved: ${integrationName}\n\n` + `**Approved actions:** ${approved.length}\n\n` + - approved.map((a) => `- \`${a.name}\`: ${a.description}`).join("\n") + + approved + .map((a) => `- \`${a.name}\`: ${a.description}`) + .join("\n") + subSchemaNote + `\n\n**Next step:** Phase 2 — use \`generatePhrases\` to create natural language samples.`, ); @@ -507,8 +579,7 @@ async function generateSubSchemaRecommendation( }, { role: "user" as const, - content: - `Categorize these ${approved.length} actions for the "${integrationName}" integration into logical sub-schema groups:\n\n${actionList}`, + content: `Categorize these ${approved.length} actions for the "${integrationName}" integration into logical sub-schema groups:\n\n${actionList}`, }, ]; diff --git a/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts b/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts index d307c48c70..898074830b 100644 --- a/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts +++ b/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts @@ -10,9 +10,7 @@ import { TypeAgentAction, ActionResult, } from "@typeagent/agent-sdk"; -import { - createActionResultFromMarkdownDisplay, -} from "@typeagent/agent-sdk/helpers/action"; +import { createActionResultFromMarkdownDisplay } from "@typeagent/agent-sdk/helpers/action"; import { GrammarGenActions } from "./grammarGenSchema.js"; import { loadState, @@ -44,40 +42,73 @@ export async function executeGrammarGenAction( } } -async function handleGenerateGrammar(integrationName: string): Promise { +async function handleGenerateGrammar( + integrationName: string, +): Promise { const state = await loadState(integrationName); if (!state) return { error: `Integration "${integrationName}" not found.` }; if (state.phases.schemaGen.status !== "approved") { - return { error: `Schema phase must be approved first. Run approveSchema.` }; + return { + error: `Schema phase must be approved first. Run approveSchema.`, + }; } - const surface = await readArtifactJson(integrationName, "discovery", "api-surface.json"); - const phraseSet = await readArtifactJson(integrationName, "phraseGen", "phrases.json"); - const schemaTs = await readArtifact(integrationName, "schemaGen", "schema.ts"); + const surface = await readArtifactJson( + integrationName, + "discovery", + "api-surface.json", + ); + const phraseSet = await readArtifactJson( + integrationName, + "phraseGen", + "phrases.json", + ); + const schemaTs = await readArtifact( + integrationName, + "schemaGen", + "schema.ts", + ); if (!surface || !phraseSet || !schemaTs) { - return { error: `Missing required artifacts for "${integrationName}".` }; + return { + error: `Missing required artifacts for "${integrationName}".`, + }; } await updatePhase(integrationName, "grammarGen", { status: "in-progress" }); const model = getGrammarGenModel(); - const prompt = buildGrammarPrompt(integrationName, surface, phraseSet, schemaTs); + const prompt = buildGrammarPrompt( + integrationName, + surface, + phraseSet, + schemaTs, + ); const result = await model.complete(prompt); if (!result.success) { return { error: `Grammar generation failed: ${result.message}` }; } const grammarContent = extractGrammarContent(result.data); - await writeArtifact(integrationName, "grammarGen", "schema.agr", grammarContent); + await writeArtifact( + integrationName, + "grammarGen", + "schema.agr", + grammarContent, + ); return createActionResultFromMarkdownDisplay( `## Grammar generated: ${integrationName}\n\n` + - "```\n" + grammarContent.slice(0, 2000) + (grammarContent.length > 2000 ? "\n// ... (truncated)" : "") + "\n```\n\n" + + "```\n" + + grammarContent.slice(0, 2000) + + (grammarContent.length > 2000 ? "\n// ... (truncated)" : "") + + "\n```\n\n" + `Use \`compileGrammar\` to validate, or \`approveGrammar\` if it looks correct.`, ); } -async function handleCompileGrammar(integrationName: string): Promise { +async function handleCompileGrammar( + integrationName: string, +): Promise { const grammarPath = path.join( getPhasePath(integrationName, "grammarGen"), "schema.agr", @@ -87,27 +118,47 @@ async function handleCompileGrammar(integrationName: string): Promise { // Resolve agc from the package's own node_modules/.bin const pkgDir = path.resolve( - fileURLToPath(import.meta.url), "..", "..", "..", + fileURLToPath(import.meta.url), + "..", + "..", + "..", ); const binDir = path.join(pkgDir, "node_modules", ".bin"); - const env = { ...process.env, PATH: binDir + path.delimiter + (process.env.PATH ?? "") }; + const env = { + ...process.env, + PATH: binDir + path.delimiter + (process.env.PATH ?? ""), + }; const proc = spawn("agc", ["-i", grammarPath, "-o", outputPath], { stdio: ["ignore", "pipe", "pipe"], @@ -117,8 +168,12 @@ async function handleCompileGrammar(integrationName: string): Promise { stdout += d.toString(); }); - proc.stderr?.on("data", (d: Buffer) => { stderr += d.toString(); }); + proc.stdout?.on("data", (d: Buffer) => { + stdout += d.toString(); + }); + proc.stderr?.on("data", (d: Buffer) => { + stderr += d.toString(); + }); proc.on("close", (code) => { if (code === 0) { @@ -126,7 +181,9 @@ async function handleCompileGrammar(integrationName: string): Promise { - resolve({ error: `Failed to run agc: ${err.message}. Is action-grammar-compiler installed?` }); + resolve({ + error: `Failed to run agc: ${err.message}. Is action-grammar-compiler installed?`, + }); }); }); } -async function handleApproveGrammar(integrationName: string): Promise { - const grammar = await readArtifact(integrationName, "grammarGen", "schema.agr"); +async function handleApproveGrammar( + integrationName: string, +): Promise { + const grammar = await readArtifact( + integrationName, + "grammarGen", + "schema.agr", + ); if (!grammar) { - return { error: `No grammar found for "${integrationName}". Run generateGrammar first.` }; + return { + error: `No grammar found for "${integrationName}". Run generateGrammar first.`, + }; } await updatePhase(integrationName, "grammarGen", { status: "approved" }); @@ -169,7 +236,10 @@ function buildGrammarPrompt( const actionExamples = surface.actions .map((a) => { const phrases = phraseSet.phrases[a.name] ?? []; - return `Action: ${a.name}\nPhrases:\n${phrases.slice(0, 4).map((p) => ` - "${p}"`).join("\n")}`; + return `Action: ${a.name}\nPhrases:\n${phrases + .slice(0, 4) + .map((p) => ` - "${p}"`) + .join("\n")}`; }) .join("\n\n"); @@ -179,7 +249,7 @@ function buildGrammarPrompt( content: "You are an expert in TypeAgent grammar files (.agr format). " + "Grammar rules use this syntax:\n" + - " = pattern -> { actionName: \"name\", parameters: { ... } }\n" + + ' = pattern -> { actionName: "name", parameters: { ... } }\n' + " | alternative -> { ... };\n\n" + "Pattern syntax:\n" + " - $(paramName:wildcard) captures 1+ words into a variable\n" + @@ -190,7 +260,7 @@ function buildGrammarPrompt( "IMPORTANT: In the action output object after ->, reference captured parameters by BARE NAME only, NOT with $() syntax.\n" + "Example:\n" + " = add $(item:wildcard) to (the)? $(listName:wildcard) list -> {\n" + - " actionName: \"addItems\",\n" + + ' actionName: "addItems",\n' + " parameters: {\n" + " items: [item],\n" + " listName\n" + @@ -198,7 +268,7 @@ function buildGrammarPrompt( " };\n\n" + "The action output must use multi-line format with proper indentation as shown above.\n" + "The file must start with a copyright header comment and end with:\n" + - " import { ActionType } from \"./schemaFile.ts\";\n" + + ' import { ActionType } from "./schemaFile.ts";\n' + " : ActionType = | | ...;\n\n" + "Respond in JSON format. Return a JSON object with a single `grammar` key containing the .agr file content as a string.", }, diff --git a/ts/packages/agents/onboarding/src/onboardingActionHandler.ts b/ts/packages/agents/onboarding/src/onboardingActionHandler.ts index 9d6e1bf4a5..b329a56f00 100644 --- a/ts/packages/agents/onboarding/src/onboardingActionHandler.ts +++ b/ts/packages/agents/onboarding/src/onboardingActionHandler.ts @@ -144,7 +144,12 @@ async function executeAction( } // Packaging phase - if (actionName === "packageAgent" || actionName === "validatePackage" || actionName === "generateDemo" || actionName === "generateReadme") { + if ( + actionName === "packageAgent" || + actionName === "validatePackage" || + actionName === "generateDemo" || + actionName === "generateReadme" + ) { return executePackagingAction( action as TypeAgentAction, context, diff --git a/ts/packages/agents/onboarding/src/packaging/packagingHandler.ts b/ts/packages/agents/onboarding/src/packaging/packagingHandler.ts index cf773609ac..7f902eada2 100644 --- a/ts/packages/agents/onboarding/src/packaging/packagingHandler.ts +++ b/ts/packages/agents/onboarding/src/packaging/packagingHandler.ts @@ -10,9 +10,7 @@ import { TypeAgentAction, ActionResult, } from "@typeagent/agent-sdk"; -import { - createActionResultFromMarkdownDisplay, -} from "@typeagent/agent-sdk/helpers/action"; +import { createActionResultFromMarkdownDisplay } from "@typeagent/agent-sdk/helpers/action"; import { PackagingActions } from "./packagingSchema.js"; import { loadState, @@ -93,11 +91,16 @@ async function handlePackageAgent( `**Agent directory:** \`${agentDir}\``, `**Build output:** \`${path.join(agentDir, "dist")}\``, ``, - buildResult.output ? `\`\`\`\n${buildResult.output.slice(0, 500)}\n\`\`\`` : "", + buildResult.output + ? `\`\`\`\n${buildResult.output.slice(0, 500)}\n\`\`\`` + : "", ]; if (register) { - const registerResult = await registerWithDispatcher(integrationName, agentDir); + const registerResult = await registerWithDispatcher( + integrationName, + agentDir, + ); summary.push(``, registerResult); } @@ -116,7 +119,9 @@ async function handlePackageAgent( return createActionResultFromMarkdownDisplay(summary.join("\n")); } -async function handleValidatePackage(integrationName: string): Promise { +async function handleValidatePackage( + integrationName: string, +): Promise { const scaffoldedTo = await readArtifact( integrationName, "scaffolder", @@ -145,10 +150,8 @@ async function handleValidatePackage(integrationName: string): Promise(integrationName, "discovery", "api-surface.json"); if (!apiSurface) { - return { error: `No approved API surface found. Complete discovery first.` }; + return { + error: `No approved API surface found. Complete discovery first.`, + }; } - const subSchemaGroups = await readArtifactJson< - Record - >(integrationName, "discovery", "sub-schema-groups.json"); + const subSchemaGroups = await readArtifactJson>( + integrationName, + "discovery", + "sub-schema-groups.json", + ); // Load the generated schema const schemaTs = await readArtifact( @@ -264,10 +273,14 @@ async function handleGenerateDemo( // Parse the LLM response — expect two fenced blocks: // ```demo ... ``` and ```narration ... ``` const responseText = result.data; - const demoScript = extractFencedBlock(responseText, "demo") ?? - extractFirstFencedBlock(responseText) ?? responseText; - const narrationScript = extractFencedBlock(responseText, "narration") ?? - extractSecondFencedBlock(responseText) ?? ""; + const demoScript = + extractFencedBlock(responseText, "demo") ?? + extractFirstFencedBlock(responseText) ?? + responseText; + const narrationScript = + extractFencedBlock(responseText, "narration") ?? + extractSecondFencedBlock(responseText) ?? + ""; // Find the scaffolded agent directory const scaffoldedTo = await readArtifact( @@ -376,10 +389,7 @@ Wrap the narration in a fenced code block with the label \`narration\`: } function extractFencedBlock(text: string, label: string): string | undefined { - const regex = new RegExp( - "```" + label + "\\s*\\n([\\s\\S]*?)\\n```", - "i", - ); + const regex = new RegExp("```" + label + "\\s*\\n([\\s\\S]*?)\\n```", "i"); const match = text.match(regex); return match?.[1]?.trim(); } @@ -420,7 +430,11 @@ async function registerWithDispatcher( name: `${integrationName}-agent`, }; - await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8"); + await fs.writeFile( + configPath, + JSON.stringify(config, null, 2), + "utf-8", + ); return `✅ Registered "${integrationName}" in dispatcher config at \`${configPath}\`\n\nRestart TypeAgent to load the new agent.`; } catch (err: any) { return `⚠️ Could not auto-register — update dispatcher config manually.\n${err?.message ?? err}`; @@ -440,8 +454,12 @@ async function runCommand( }); let output = ""; - proc.stdout?.on("data", (d: Buffer) => { output += d.toString(); }); - proc.stderr?.on("data", (d: Buffer) => { output += d.toString(); }); + proc.stdout?.on("data", (d: Buffer) => { + output += d.toString(); + }); + proc.stderr?.on("data", (d: Buffer) => { + output += d.toString(); + }); proc.on("close", (code) => { resolve({ success: code === 0, output }); @@ -467,9 +485,14 @@ async function handleGenerateReadme( recommended: boolean; groups: { name: string; description: string; actions: string[] }[]; }>(integrationName, "discovery", "sub-schema-groups.json"); - const scaffoldedTo = await readArtifact(integrationName, "scaffolder", "scaffolded-to.txt"); + const scaffoldedTo = await readArtifact( + integrationName, + "scaffolder", + "scaffolded-to.txt", + ); - const description = state.config.description ?? `Agent for ${integrationName}`; + const description = + state.config.description ?? `Agent for ${integrationName}`; const totalActions = surface?.actions.length ?? 0; // Build action listing for the LLM @@ -551,7 +574,12 @@ async function handleGenerateReadme( } // Save as artifact - await writeArtifact(integrationName, "packaging", "README.md", readmeContent); + await writeArtifact( + integrationName, + "packaging", + "README.md", + readmeContent, + ); return createActionResultFromMarkdownDisplay( `## README generated: ${integrationName}\n\n` + diff --git a/ts/packages/agents/onboarding/src/phraseGen/phraseGenHandler.ts b/ts/packages/agents/onboarding/src/phraseGen/phraseGenHandler.ts index 7f7a452faf..a98a934217 100644 --- a/ts/packages/agents/onboarding/src/phraseGen/phraseGenHandler.ts +++ b/ts/packages/agents/onboarding/src/phraseGen/phraseGenHandler.ts @@ -74,7 +74,9 @@ async function handleGeneratePhrases( return { error: `Integration "${integrationName}" not found.` }; } if (state.phases.discovery.status !== "approved") { - return { error: `Discovery phase must be approved before generating phrases. Run approveApiSurface first.` }; + return { + error: `Discovery phase must be approved before generating phrases. Run approveApiSurface first.`, + }; } const surface = await readArtifactJson( @@ -123,7 +125,12 @@ async function handleGeneratePhrases( phrases: phraseMap, }; - await writeArtifactJson(integrationName, "phraseGen", "phrases.json", phraseSet); + await writeArtifactJson( + integrationName, + "phraseGen", + "phrases.json", + phraseSet, + ); const totalPhrases = Object.values(phraseMap).reduce( (sum, p) => sum + p.length, @@ -166,7 +173,10 @@ async function handleAddPhrase( } await writeArtifactJson(integrationName, "phraseGen", "phrases.json", { - ...(existing ?? { integrationName, generatedAt: new Date().toISOString() }), + ...(existing ?? { + integrationName, + generatedAt: new Date().toISOString(), + }), phrases: phraseMap, }); @@ -191,7 +201,12 @@ async function handleRemovePhrase( const phrases = existing.phrases[actionName] ?? []; existing.phrases[actionName] = phrases.filter((p) => p !== phrase); - await writeArtifactJson(integrationName, "phraseGen", "phrases.json", existing); + await writeArtifactJson( + integrationName, + "phraseGen", + "phrases.json", + existing, + ); return createActionResultFromTextDisplay( `Removed phrase "${phrase}" from action "${actionName}" for ${integrationName}.`, @@ -207,7 +222,9 @@ async function handleApprovePhrases( "phrases.json", ); if (!phraseSet) { - return { error: `No phrases found for "${integrationName}". Run generatePhrases first.` }; + return { + error: `No phrases found for "${integrationName}". Run generatePhrases first.`, + }; } const updated: PhraseSet = { @@ -216,7 +233,12 @@ async function handleApprovePhrases( approvedAt: new Date().toISOString(), }; - await writeArtifactJson(integrationName, "phraseGen", "phrases.json", updated); + await writeArtifactJson( + integrationName, + "phraseGen", + "phrases.json", + updated, + ); await updatePhase(integrationName, "phraseGen", { status: "approved" }); const totalPhrases = Object.values(phraseSet.phrases).reduce( diff --git a/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts b/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts index 25ff0784fb..9c834f903a 100644 --- a/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts +++ b/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts @@ -10,9 +10,7 @@ import { TypeAgentAction, ActionResult, } from "@typeagent/agent-sdk"; -import { - createActionResultFromMarkdownDisplay, -} from "@typeagent/agent-sdk/helpers/action"; +import { createActionResultFromMarkdownDisplay } from "@typeagent/agent-sdk/helpers/action"; import { ScaffolderActions } from "./scaffolderSchema.js"; import { loadState, @@ -71,13 +69,25 @@ async function handleScaffoldAgent( const state = await loadState(integrationName); if (!state) return { error: `Integration "${integrationName}" not found.` }; if (state.phases.grammarGen.status !== "approved") { - return { error: `Grammar phase must be approved first. Run approveGrammar.` }; + return { + error: `Grammar phase must be approved first. Run approveGrammar.`, + }; } - const schemaTs = await readArtifact(integrationName, "schemaGen", "schema.ts"); - const grammarAgr = await readArtifact(integrationName, "grammarGen", "schema.agr"); + const schemaTs = await readArtifact( + integrationName, + "schemaGen", + "schema.ts", + ); + const grammarAgr = await readArtifact( + integrationName, + "grammarGen", + "schema.agr", + ); if (!schemaTs || !grammarAgr) { - return { error: `Missing schema or grammar artifacts for "${integrationName}".` }; + return { + error: `Missing schema or grammar artifacts for "${integrationName}".`, + }; } await updatePhase(integrationName, "scaffolder", { status: "in-progress" }); @@ -91,22 +101,19 @@ async function handleScaffoldAgent( await fs.mkdir(srcDir, { recursive: true }); // Check if sub-schema groups exist from the discovery phase - const subSchemaSuggestion = - await readArtifactJson( - integrationName, - "discovery", - "sub-schema-groups.json", - ); + const subSchemaSuggestion = await readArtifactJson( + integrationName, + "discovery", + "sub-schema-groups.json", + ); const subGroups = - subSchemaSuggestion?.recommended && subSchemaSuggestion.groups.length > 0 + subSchemaSuggestion?.recommended && + subSchemaSuggestion.groups.length > 0 ? subSchemaSuggestion.groups : undefined; // Write core schema and grammar - await writeFile( - path.join(srcDir, `${integrationName}Schema.ts`), - schemaTs, - ); + await writeFile(path.join(srcDir, `${integrationName}Schema.ts`), schemaTs); await writeFile( path.join(srcDir, `${integrationName}Schema.agr`), grammarAgr.replace( @@ -186,7 +193,12 @@ async function handleScaffoldAgent( await writeFile( path.join(targetDir, "package.json"), JSON.stringify( - buildPackageJson(integrationName, packageName, pascalName, subSchemaNames), + buildPackageJson( + integrationName, + packageName, + pascalName, + subSchemaNames, + ), null, 2, ), @@ -337,7 +349,9 @@ async function handleScaffoldPlugin( const templateInfo = PLUGIN_TEMPLATES[template]; if (!templateInfo) { - return { error: `Unknown template "${template}". Use listTemplates to see available templates.` }; + return { + error: `Unknown template "${template}". Use listTemplates to see available templates.`, + }; } const targetDir = diff --git a/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.ts b/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.ts index f6ae49263c..3dedf17ea8 100644 --- a/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.ts +++ b/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.ts @@ -22,7 +22,12 @@ export type ScaffoldPluginAction = { // Integration name to scaffold the host-side plugin for integrationName: string; // Template to use for the plugin side - template: "office-addin" | "vscode-extension" | "electron-app" | "browser-extension" | "rest-client"; + template: + | "office-addin" + | "vscode-extension" + | "electron-app" + | "browser-extension" + | "rest-client"; // Target directory for the plugin (defaults to ts/packages/agents//plugin) outputDir?: string; }; diff --git a/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts b/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts index f2a4ecfaaa..db58175b44 100644 --- a/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts +++ b/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts @@ -10,9 +10,7 @@ import { TypeAgentAction, ActionResult, } from "@typeagent/agent-sdk"; -import { - createActionResultFromMarkdownDisplay, -} from "@typeagent/agent-sdk/helpers/action"; +import { createActionResultFromMarkdownDisplay } from "@typeagent/agent-sdk/helpers/action"; import { SchemaGenActions } from "./schemaGenSchema.js"; import { loadState, @@ -73,24 +71,43 @@ export async function executeSchemaGenAction( } } -async function handleGenerateSchema(integrationName: string): Promise { +async function handleGenerateSchema( + integrationName: string, +): Promise { const state = await loadState(integrationName); if (!state) return { error: `Integration "${integrationName}" not found.` }; if (state.phases.discovery.status !== "approved") { - return { error: `Discovery phase must be approved first. Run approveApiSurface.` }; + return { + error: `Discovery phase must be approved first. Run approveApiSurface.`, + }; } - const surface = await readArtifactJson(integrationName, "discovery", "api-surface.json"); + const surface = await readArtifactJson( + integrationName, + "discovery", + "api-surface.json", + ); if (!surface) { - return { error: `Missing discovery artifact for "${integrationName}".` }; + return { + error: `Missing discovery artifact for "${integrationName}".`, + }; } // phraseSet is optional — we can still generate a schema without sample phrases - const phraseSet = await readArtifactJson(integrationName, "phraseGen", "phrases.json"); + const phraseSet = await readArtifactJson( + integrationName, + "phraseGen", + "phrases.json", + ); await updatePhase(integrationName, "schemaGen", { status: "in-progress" }); const model = getSchemaGenModel(); - const prompt = buildSchemaPrompt(integrationName, surface, phraseSet ?? null, state.config.description); + const prompt = buildSchemaPrompt( + integrationName, + surface, + phraseSet ?? null, + state.config.description, + ); const result = await model.complete(prompt); if (!result.success) { return { error: `Schema generation failed: ${result.message}` }; @@ -101,7 +118,10 @@ async function handleGenerateSchema(integrationName: string): Promise 2000 ? "\n// ... (truncated)" : "") + "\n```\n\n" + + "```typescript\n" + + schemaTs.slice(0, 2000) + + (schemaTs.length > 2000 ? "\n// ... (truncated)" : "") + + "\n```\n\n" + `Use \`refineSchema\` to adjust, or \`approveSchema\` to proceed to grammar generation.`, ); } @@ -110,9 +130,15 @@ async function handleRefineSchema( integrationName: string, instructions: string, ): Promise { - const existing = await readArtifact(integrationName, "schemaGen", "schema.ts"); + const existing = await readArtifact( + integrationName, + "schemaGen", + "schema.ts", + ); if (!existing) { - return { error: `No schema found for "${integrationName}". Run generateSchema first.` }; + return { + error: `No schema found for "${integrationName}". Run generateSchema first.`, + }; } const model = getSchemaGenModel(); @@ -142,20 +168,36 @@ async function handleRefineSchema( const refined = extractTypeScript(result.data); // Archive the previous version const version = Date.now(); - await writeArtifact(integrationName, "schemaGen", `schema.v${version}.ts`, existing); + await writeArtifact( + integrationName, + "schemaGen", + `schema.v${version}.ts`, + existing, + ); await writeArtifact(integrationName, "schemaGen", "schema.ts", refined); return createActionResultFromMarkdownDisplay( `## Schema refined: ${integrationName}\n\n` + `Previous version archived as \`schema.v${version}.ts\`\n\n` + - "```typescript\n" + refined.slice(0, 2000) + (refined.length > 2000 ? "\n// ... (truncated)" : "") + "\n```", + "```typescript\n" + + refined.slice(0, 2000) + + (refined.length > 2000 ? "\n// ... (truncated)" : "") + + "\n```", ); } -async function handleApproveSchema(integrationName: string): Promise { - const schema = await readArtifact(integrationName, "schemaGen", "schema.ts"); +async function handleApproveSchema( + integrationName: string, +): Promise { + const schema = await readArtifact( + integrationName, + "schemaGen", + "schema.ts", + ); if (!schema) { - return { error: `No schema found for "${integrationName}". Run generateSchema first.` }; + return { + error: `No schema found for "${integrationName}". Run generateSchema first.`, + }; } await updatePhase(integrationName, "schemaGen", { status: "approved" }); @@ -183,7 +225,10 @@ function buildSchemaPrompt( ? `Parameters: ${a.parameters.map((p) => `${p.name}: ${p.type}${p.required ? "" : "?"}`).join(", ")}\n` : "") + (phrases.length - ? `Sample phrases:\n${phrases.slice(0, 3).map((p) => ` - "${p}"`).join("\n")}` + ? `Sample phrases:\n${phrases + .slice(0, 3) + .map((p) => ` - "${p}"`) + .join("\n")}` : "") ); }) @@ -199,7 +244,7 @@ function buildSchemaPrompt( "- Start the file with:\n // Copyright (c) Microsoft Corporation.\n // Licensed under the MIT License.\n" + "- Export a top-level union type named `Actions`\n" + "- Each action type is named `Action`\n" + - "- Use `actionName: \"camelCaseName\"` as a string literal type\n" + + '- Use `actionName: "camelCaseName"` as a string literal type\n' + "- Parameters use camelCase names\n" + "- Optional parameters use `?: type` syntax\n" + SCHEMA_GUIDELINES + @@ -225,7 +270,9 @@ function extractTypeScript(llmResponse: string): string { // Not JSON, fall through to other extraction methods } // Strip markdown code fences if present - const fenceMatch = llmResponse.match(/```(?:typescript|ts)?\n([\s\S]*?)```/); + const fenceMatch = llmResponse.match( + /```(?:typescript|ts)?\n([\s\S]*?)```/, + ); if (fenceMatch) return fenceMatch[1].trim(); return llmResponse.trim(); } diff --git a/ts/packages/agents/onboarding/src/testing/testingHandler.ts b/ts/packages/agents/onboarding/src/testing/testingHandler.ts index 604b1db291..8953268d78 100644 --- a/ts/packages/agents/onboarding/src/testing/testingHandler.ts +++ b/ts/packages/agents/onboarding/src/testing/testingHandler.ts @@ -39,7 +39,11 @@ import type { RequestId, CommandResult, } from "@typeagent/dispatcher-types"; -import type { DisplayAppendMode, DisplayContent, MessageContent } from "@typeagent/agent-sdk"; +import type { + DisplayAppendMode, + DisplayContent, + MessageContent, +} from "@typeagent/agent-sdk"; export type TestCase = { phrase: string; @@ -111,11 +115,15 @@ export async function executeTestingAction( } } -async function handleGenerateTests(integrationName: string): Promise { +async function handleGenerateTests( + integrationName: string, +): Promise { const state = await loadState(integrationName); if (!state) return { error: `Integration "${integrationName}" not found.` }; if (state.phases.scaffolder.status !== "approved") { - return { error: `Scaffolder phase must be approved first. Run scaffoldAgent.` }; + return { + error: `Scaffolder phase must be approved first. Run scaffoldAgent.`, + }; } const phraseSet = await readArtifactJson( @@ -167,7 +175,9 @@ async function handleRunTests( "test-cases.json", ); if (!testCases || testCases.length === 0) { - return { error: `No test cases found for "${integrationName}". Run generateTests first.` }; + return { + error: `No test cases found for "${integrationName}". Run generateTests first.`, + }; } let toRun = forActions @@ -178,7 +188,9 @@ async function handleRunTests( // Create a dispatcher and run each phrase through it. // The scaffolded agent must be registered in config.json before running tests. // Use `packageAgent --register` (phase 7) or add manually and restart TypeAgent. - let dispatcherSession: Awaited> | undefined; + let dispatcherSession: + | Awaited> + | undefined; try { dispatcherSession = await createTestDispatcher(); } catch (err: any) { @@ -192,7 +204,11 @@ async function handleRunTests( const results: TestResult[] = []; for (const tc of toRun) { - const result = await runSingleTest(tc, integrationName, dispatcherSession.dispatcher); + const result = await runSingleTest( + tc, + integrationName, + dispatcherSession.dispatcher, + ); results.push(result); } @@ -210,7 +226,12 @@ async function handleRunTests( results, }; - await writeArtifactJson(integrationName, "testing", "results.json", testRun); + await writeArtifactJson( + integrationName, + "testing", + "results.json", + testRun, + ); const passRate = Math.round((passed / results.length) * 100); @@ -243,7 +264,9 @@ async function handleGetTestResults( "results.json", ); if (!testRun) { - return { error: `No test results found for "${integrationName}". Run runTests first.` }; + return { + error: `No test results found for "${integrationName}". Run runTests first.`, + }; } const results = filter @@ -260,10 +283,12 @@ async function handleGetTestResults( ``, `| Result | Phrase | Expected | Actual |`, `|---|---|---|---|`, - ...results.slice(0, 50).map( - (r) => - `| ${r.passed ? "✅" : "❌"} | "${r.phrase}" | \`${r.expectedActionName}\` | \`${r.actualActionName ?? r.error ?? "—"}\` |`, - ), + ...results + .slice(0, 50) + .map( + (r) => + `| ${r.passed ? "✅" : "❌"} | "${r.phrase}" | \`${r.expectedActionName}\` | \`${r.actualActionName ?? r.error ?? "—"}\` |`, + ), ]; if (results.length > 50) { lines.push(``, `_...and ${results.length - 50} more_`); @@ -292,8 +317,16 @@ async function handleProposeRepair( ); } - const schemaTs = await readArtifact(integrationName, "schemaGen", "schema.ts"); - const grammarAgr = await readArtifact(integrationName, "grammarGen", "schema.agr"); + const schemaTs = await readArtifact( + integrationName, + "schemaGen", + "schema.ts", + ); + const grammarAgr = await readArtifact( + integrationName, + "grammarGen", + "schema.agr", + ); const filteredFailing = forActions ? failing.filter((r) => forActions.includes(r.expectedActionName)) @@ -332,8 +365,12 @@ async function handleProposeRepair( }; // Extract suggested schema and grammar changes from the response - const schemaMatch = schemaFromJson ? null : result.data.match(/```typescript([\s\S]*?)```/); - const grammarMatch = grammarFromJson ? null : result.data.match(/```(?:agr)?([\s\S]*?)```/); + const schemaMatch = schemaFromJson + ? null + : result.data.match(/```typescript([\s\S]*?)```/); + const grammarMatch = grammarFromJson + ? null + : result.data.match(/```(?:agr)?([\s\S]*?)```/); if (schemaFromJson) repair.schemaChanges = schemaFromJson.trim(); else if (schemaMatch) repair.schemaChanges = schemaMatch[1].trim(); if (grammarFromJson) repair.grammarChanges = grammarFromJson.trim(); @@ -355,7 +392,9 @@ async function handleProposeRepair( ); } -async function handleApproveRepair(integrationName: string): Promise { +async function handleApproveRepair( + integrationName: string, +): Promise { const repair = await readArtifactJson( integrationName, "testing", @@ -371,7 +410,11 @@ async function handleApproveRepair(integrationName: string): Promise r.join(" | ")).join("\n"); } // TypedDisplayContent @@ -452,7 +515,9 @@ function createCapturingClientIO(buffer: string[]): ClientIO { setDynamicDisplay: noop, askYesNo: async (_id: RequestId, _msg: string, def = false) => def, proposeAction: async () => undefined, - popupQuestion: async () => { throw new Error("popupQuestion not supported in test runner"); }, + popupQuestion: async () => { + throw new Error("popupQuestion not supported in test runner"); + }, notify: noop, openLocalView: async () => {}, closeLocalView: async () => {}, @@ -468,7 +533,8 @@ function getExternalAppAgentProviders(instanceDir: string) { const configPath = path.join(instanceDir, "externalAgentsConfig.json"); // eslint-disable-next-line @typescript-eslint/no-explicit-any const agents = fs.existsSync(configPath) - ? (JSON.parse(fs.readFileSync(configPath, "utf8")) as any).agents ?? {} + ? ((JSON.parse(fs.readFileSync(configPath, "utf8")) as any).agents ?? + {}) : {}; return [ createNpmAppAgentProvider( diff --git a/ts/packages/commandExecutor/src/commandServer.ts b/ts/packages/commandExecutor/src/commandServer.ts index ea09903be0..8190df6035 100644 --- a/ts/packages/commandExecutor/src/commandServer.ts +++ b/ts/packages/commandExecutor/src/commandServer.ts @@ -537,7 +537,7 @@ export class CommandServer { .enum(["reconnect", "full"]) .optional() .describe( - 'reconnect: disconnect and reconnect to the agent server (default). full: exit the MCP server process so the client can restart it (picks up new MCP server code).', + "reconnect: disconnect and reconnect to the agent server (default). full: exit the MCP server process so the client can restart it (picks up new MCP server code).", ), }, description: @@ -548,9 +548,7 @@ export class CommandServer { ); } - private async restart( - mode: "reconnect" | "full", - ): Promise { + private async restart(mode: "reconnect" | "full"): Promise { if (mode === "full") { this.logger.log("Full restart requested — exiting process."); // Give time for the response to be sent before exiting From fb9f4b1a9b1e53b01eb646b8b85107b839fff63a Mon Sep 17 00:00:00 2001 From: robgruen Date: Mon, 13 Apr 2026 14:21:24 -0700 Subject: [PATCH 20/33] updated lock file --- ts/pnpm-lock.yaml | 170 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml index a368486bf4..be475ea6f0 100644 --- a/ts/pnpm-lock.yaml +++ b/ts/pnpm-lock.yaml @@ -1888,6 +1888,40 @@ importers: specifier: ~5.4.5 version: 5.4.5 + packages/agents/excel: + dependencies: + '@typeagent/agent-sdk': + specifier: workspace:* + version: link:../../agentSdk + aiclient: + specifier: workspace:* + version: link:../../aiclient + ws: + specifier: ^8.18.0 + version: 8.19.0 + devDependencies: + '@typeagent/action-schema-compiler': + specifier: workspace:* + version: link:../../actionSchemaCompiler + '@types/ws': + specifier: ^8.5.12 + version: 8.18.1 + action-grammar-compiler: + specifier: workspace:* + version: link:../../actionGrammarCompiler + concurrently: + specifier: ^9.1.2 + version: 9.1.2 + prettier: + specifier: ^3.5.3 + version: 3.5.3 + rimraf: + specifier: ^6.0.1 + version: 6.0.1 + typescript: + specifier: ~5.4.5 + version: 5.4.5 + packages/agents/greeting: dependencies: '@typeagent/agent-sdk': @@ -2232,6 +2266,74 @@ importers: specifier: ^5.2.0 version: 5.2.1(debug@4.4.1)(webpack-cli@5.1.4)(webpack@5.105.0) + packages/agents/onboarding: + dependencies: + '@typeagent/agent-sdk': + specifier: workspace:* + version: link:../../agentSdk + '@typeagent/dispatcher-types': + specifier: workspace:* + version: link:../../dispatcher/types + agent-dispatcher: + specifier: workspace:* + version: link:../../dispatcher/dispatcher + aiclient: + specifier: workspace:* + version: link:../../aiclient + dispatcher-node-providers: + specifier: workspace:* + version: link:../../dispatcher/nodeProviders + typechat: + specifier: ^0.1.1 + version: 0.1.1(typescript@5.4.5)(zod@3.25.76) + devDependencies: + '@typeagent/action-schema-compiler': + specifier: workspace:* + version: link:../../actionSchemaCompiler + action-grammar-compiler: + specifier: workspace:* + version: link:../../actionGrammarCompiler + concurrently: + specifier: ^9.1.2 + version: 9.1.2 + prettier: + specifier: ^3.5.3 + version: 3.5.3 + rimraf: + specifier: ^6.0.1 + version: 6.0.1 + typescript: + specifier: ~5.4.5 + version: 5.4.5 + + packages/agents/outlook: + dependencies: + '@typeagent/agent-sdk': + specifier: workspace:* + version: link:../../agentSdk + ws: + specifier: ^8.18.0 + version: 8.19.0 + devDependencies: + '@typeagent/action-schema-compiler': + specifier: workspace:* + version: link:../../actionSchemaCompiler + '@types/ws': + specifier: ^8.5.12 + version: 8.18.1 + action-grammar-compiler: + specifier: workspace:* + version: link:../../actionGrammarCompiler + concurrently: + specifier: ^9.1.2 + version: 9.1.2 + rimraf: + specifier: ^6.0.1 + version: 6.0.1 + typescript: + specifier: ~5.4.5 + version: 5.4.5 + packages/agents/photo: dependencies: '@typeagent/agent-sdk': @@ -2352,6 +2454,40 @@ importers: specifier: ~5.4.5 version: 5.4.5 + packages/agents/powerpoint: + dependencies: + '@typeagent/agent-sdk': + specifier: workspace:* + version: link:../../agentSdk + ws: + specifier: ^8.17.1 + version: 8.19.0 + devDependencies: + '@typeagent/action-schema-compiler': + specifier: workspace:* + version: link:../../actionSchemaCompiler + '@types/node': + specifier: ^20.10.4 + version: 20.19.39 + '@types/ws': + specifier: ^8.5.10 + version: 8.18.1 + action-grammar-compiler: + specifier: workspace:* + version: link:../../actionGrammarCompiler + concurrently: + specifier: ^9.1.2 + version: 9.1.2 + prettier: + specifier: ^3.5.3 + version: 3.5.3 + rimraf: + specifier: ^6.0.1 + version: 6.0.1 + typescript: + specifier: ~5.4.5 + version: 5.4.5 + packages/agents/scriptflow: dependencies: '@anthropic-ai/claude-agent-sdk': @@ -2673,6 +2809,37 @@ importers: specifier: ~5.4.5 version: 5.4.5 + packages/agents/word: + dependencies: + '@typeagent/agent-sdk': + specifier: workspace:* + version: link:../../agentSdk + ws: + specifier: ^8.18.0 + version: 8.19.0 + devDependencies: + '@typeagent/action-schema-compiler': + specifier: workspace:* + version: link:../../actionSchemaCompiler + '@types/ws': + specifier: ^8.5.12 + version: 8.18.1 + action-grammar-compiler: + specifier: workspace:* + version: link:../../actionGrammarCompiler + concurrently: + specifier: ^9.1.2 + version: 9.1.2 + prettier: + specifier: ^3.5.3 + version: 3.5.3 + rimraf: + specifier: ^6.0.1 + version: 6.0.1 + typescript: + specifier: ~5.4.5 + version: 5.4.5 + packages/aiclient: dependencies: '@azure/identity': @@ -3364,6 +3531,9 @@ importers: music-local: specifier: workspace:* version: link:../agents/playerLocal + onboarding-agent: + specifier: workspace:* + version: link:../agents/onboarding photo-agent: specifier: workspace:* version: link:../agents/photo From 8ee1b0ab492f18799414b8a93b6359063508a8ca Mon Sep 17 00:00:00 2001 From: robgruen Date: Mon, 13 Apr 2026 15:17:13 -0700 Subject: [PATCH 21/33] Potential fix for pull request finding 'CodeQL / Incomplete multi-character sanitization' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../onboarding/src/discovery/discoveryHandler.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts b/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts index 7063be6c72..1e96ec83df 100644 --- a/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts +++ b/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts @@ -209,9 +209,18 @@ async function handleCrawlDocUrl( // Strip HTML tags and collapse whitespace to extract readable text. function stripHtml(html: string): string { - return html - .replace(//gi, "") - .replace(//gi, "") + // Repeatedly remove multi-character patterns until stable to avoid + // incomplete sanitization from overlapping/re-formed substrings. + let sanitized = html; + let previous: string; + do { + previous = sanitized; + sanitized = sanitized + .replace(//gi, "") + .replace(//gi, ""); + } while (sanitized !== previous); + + return sanitized .replace(/<[^>]+>/g, " ") .replace(/ /g, " ") .replace(/</g, "<") From e1bbf24c9c649f4447fccc1a2c1ac70d6bc9a468 Mon Sep 17 00:00:00 2001 From: robgruen Date: Mon, 13 Apr 2026 15:17:43 -0700 Subject: [PATCH 22/33] Potential fix for pull request finding 'CodeQL / Double escaping or unescaping' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts b/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts index 1e96ec83df..79faa7ccf7 100644 --- a/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts +++ b/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts @@ -225,8 +225,8 @@ function stripHtml(html: string): string { .replace(/ /g, " ") .replace(/</g, "<") .replace(/>/g, ">") - .replace(/&/g, "&") .replace(/"/g, '"') + .replace(/&/g, "&") .replace(/\s{2,}/g, " ") .trim(); } From fa4d631535ccce88b09813e722afd389d7e17aa3 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Mon, 13 Apr 2026 15:36:49 -0700 Subject: [PATCH 23/33] lint --- .../onboarding/src/testing/testingHandler.ts | 3 + ts/pnpm-lock.yaml | 127 ------------------ ts/tools/scripts/fix-dependabot-alerts.mjs | 28 ++-- 3 files changed, 22 insertions(+), 136 deletions(-) diff --git a/ts/packages/agents/onboarding/src/testing/testingHandler.ts b/ts/packages/agents/onboarding/src/testing/testingHandler.ts index 8953268d78..f182d7d80f 100644 --- a/ts/packages/agents/onboarding/src/testing/testingHandler.ts +++ b/ts/packages/agents/onboarding/src/testing/testingHandler.ts @@ -522,6 +522,9 @@ function createCapturingClientIO(buffer: string[]): ClientIO { openLocalView: async () => {}, closeLocalView: async () => {}, requestChoice: noop, + requestInteraction: noop, + interactionResolved: noop, + interactionCancelled: noop, takeAction: noop, } satisfies ClientIO; } diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml index be475ea6f0..6e62a19304 100644 --- a/ts/pnpm-lock.yaml +++ b/ts/pnpm-lock.yaml @@ -1888,40 +1888,6 @@ importers: specifier: ~5.4.5 version: 5.4.5 - packages/agents/excel: - dependencies: - '@typeagent/agent-sdk': - specifier: workspace:* - version: link:../../agentSdk - aiclient: - specifier: workspace:* - version: link:../../aiclient - ws: - specifier: ^8.18.0 - version: 8.19.0 - devDependencies: - '@typeagent/action-schema-compiler': - specifier: workspace:* - version: link:../../actionSchemaCompiler - '@types/ws': - specifier: ^8.5.12 - version: 8.18.1 - action-grammar-compiler: - specifier: workspace:* - version: link:../../actionGrammarCompiler - concurrently: - specifier: ^9.1.2 - version: 9.1.2 - prettier: - specifier: ^3.5.3 - version: 3.5.3 - rimraf: - specifier: ^6.0.1 - version: 6.0.1 - typescript: - specifier: ~5.4.5 - version: 5.4.5 - packages/agents/greeting: dependencies: '@typeagent/agent-sdk': @@ -2306,34 +2272,6 @@ importers: specifier: ~5.4.5 version: 5.4.5 - packages/agents/outlook: - dependencies: - '@typeagent/agent-sdk': - specifier: workspace:* - version: link:../../agentSdk - ws: - specifier: ^8.18.0 - version: 8.19.0 - devDependencies: - '@typeagent/action-schema-compiler': - specifier: workspace:* - version: link:../../actionSchemaCompiler - '@types/ws': - specifier: ^8.5.12 - version: 8.18.1 - action-grammar-compiler: - specifier: workspace:* - version: link:../../actionGrammarCompiler - concurrently: - specifier: ^9.1.2 - version: 9.1.2 - rimraf: - specifier: ^6.0.1 - version: 6.0.1 - typescript: - specifier: ~5.4.5 - version: 5.4.5 - packages/agents/photo: dependencies: '@typeagent/agent-sdk': @@ -2454,40 +2392,6 @@ importers: specifier: ~5.4.5 version: 5.4.5 - packages/agents/powerpoint: - dependencies: - '@typeagent/agent-sdk': - specifier: workspace:* - version: link:../../agentSdk - ws: - specifier: ^8.17.1 - version: 8.19.0 - devDependencies: - '@typeagent/action-schema-compiler': - specifier: workspace:* - version: link:../../actionSchemaCompiler - '@types/node': - specifier: ^20.10.4 - version: 20.19.39 - '@types/ws': - specifier: ^8.5.10 - version: 8.18.1 - action-grammar-compiler: - specifier: workspace:* - version: link:../../actionGrammarCompiler - concurrently: - specifier: ^9.1.2 - version: 9.1.2 - prettier: - specifier: ^3.5.3 - version: 3.5.3 - rimraf: - specifier: ^6.0.1 - version: 6.0.1 - typescript: - specifier: ~5.4.5 - version: 5.4.5 - packages/agents/scriptflow: dependencies: '@anthropic-ai/claude-agent-sdk': @@ -2809,37 +2713,6 @@ importers: specifier: ~5.4.5 version: 5.4.5 - packages/agents/word: - dependencies: - '@typeagent/agent-sdk': - specifier: workspace:* - version: link:../../agentSdk - ws: - specifier: ^8.18.0 - version: 8.19.0 - devDependencies: - '@typeagent/action-schema-compiler': - specifier: workspace:* - version: link:../../actionSchemaCompiler - '@types/ws': - specifier: ^8.5.12 - version: 8.18.1 - action-grammar-compiler: - specifier: workspace:* - version: link:../../actionGrammarCompiler - concurrently: - specifier: ^9.1.2 - version: 9.1.2 - prettier: - specifier: ^3.5.3 - version: 3.5.3 - rimraf: - specifier: ^6.0.1 - version: 6.0.1 - typescript: - specifier: ~5.4.5 - version: 5.4.5 - packages/aiclient: dependencies: '@azure/identity': diff --git a/ts/tools/scripts/fix-dependabot-alerts.mjs b/ts/tools/scripts/fix-dependabot-alerts.mjs index 482f7aa45d..5903ce70ab 100644 --- a/ts/tools/scripts/fix-dependabot-alerts.mjs +++ b/ts/tools/scripts/fix-dependabot-alerts.mjs @@ -23,13 +23,16 @@ function detectWorkspaceRoot() { try { const gitRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8", - }).trim().replace(/\\/g, "/"); + }) + .trim() + .replace(/\\/g, "/"); const cwdNorm = process.cwd().replace(/\\/g, "/"); - const rel = cwdNorm === gitRoot - ? "" - : cwdNorm.startsWith(gitRoot + "/") - ? cwdNorm.slice(gitRoot.length + 1) - : ""; + const rel = + cwdNorm === gitRoot + ? "" + : cwdNorm.startsWith(gitRoot + "/") + ? cwdNorm.slice(gitRoot.length + 1) + : ""; const wsPrefix = rel ? rel.split("/")[0] : ""; return wsPrefix ? resolve(gitRoot, wsPrefix) : resolve(cwdNorm); } catch { @@ -2105,13 +2108,18 @@ function fetchAlerts() { // even when the script is invoked from a subdirectory like ts/tools). let wsPrefix = ""; try { - const gitRoot = runCmd("git", ["rev-parse", "--show-toplevel"]).replace(/\\/g, "/"); + const gitRoot = runCmd("git", ["rev-parse", "--show-toplevel"]).replace( + /\\/g, + "/", + ); const rootNorm = ROOT.replace(/\\/g, "/"); wsPrefix = rootNorm.startsWith(gitRoot + "/") ? rootNorm.slice(gitRoot.length + 1).split("/")[0] : ""; } catch { - verbose(" Could not determine git root; skipping workspace-specific alert filtering."); + verbose( + " Could not determine git root; skipping workspace-specific alert filtering.", + ); } if (wsPrefix) { const before = alerts.length; @@ -2120,7 +2128,9 @@ function fetchAlerts() { return manifest.startsWith(wsPrefix + "/") || manifest === wsPrefix; }); if (alerts.length < before) { - verbose(` Filtered to ${alerts.length}/${before} alerts matching workspace "${wsPrefix}/"`); + verbose( + ` Filtered to ${alerts.length}/${before} alerts matching workspace "${wsPrefix}/"`, + ); } } From 5515515014d20451f63ea74532e0cb47be65995b Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Mon, 13 Apr 2026 15:44:26 -0700 Subject: [PATCH 24/33] updated instructions and TODOs --- ts/packages/agents/onboarding/README.md | 53 +++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/ts/packages/agents/onboarding/README.md b/ts/packages/agents/onboarding/README.md index 534325fb4e..57306a3b94 100644 --- a/ts/packages/agents/onboarding/README.md +++ b/ts/packages/agents/onboarding/README.md @@ -72,6 +72,43 @@ run tests for slack Each phase must be **approved** before the next phase begins. Approval locks the phase's artifacts and advances the current phase pointer in `state.json`. +## For Best Results + +The onboarding agent is designed to be driven by an AI orchestrator (Claude Code, GitHub Copilot) that can call TypeAgent actions iteratively, inspect artifacts, and guide each phase to completion. For the best experience, set up TypeAgent as an MCP server so your AI client can communicate with it directly. + +### Set up TypeAgent as an MCP server + +TypeAgent exposes a **Command Executor MCP server** that bridges any MCP-compatible client (Claude Code, GitHub Copilot) to the TypeAgent dispatcher. Full setup instructions are in [packages/commandExecutor/README.md](../../commandExecutor/README.md). The short version: + +1. **Build** the workspace (from `ts/`): + + ```bash + pnpm run build + ``` + +2. **Add the MCP server** to `.mcp.json` at the repo root (create it if it doesn't exist): + + ```json + { + "mcpServers": { + "command-executor": { + "command": "node", + "args": ["packages/commandExecutor/dist/server.js"] + } + } + } + ``` + +3. **Start the TypeAgent dispatcher** (in a separate terminal): + + ```bash + pnpm run start:agent-server + ``` + +4. **Restart your AI client** (Claude Code or Copilot) to pick up the new MCP configuration. + +Once connected, your AI client can drive onboarding phases end-to-end using natural language — e.g. *"start onboarding for Slack"* — without any manual copy-paste between tools. + ## Building ```bash @@ -79,6 +116,22 @@ pnpm install pnpm run build ``` +## TODO + +### Additional discovery crawlers + +The discovery phase currently supports web docs and OpenAPI specs. Planned crawlers: + +- **CLI `--help` scraping** — invoke a command-line tool with `--help` / `--help ` and parse the output to enumerate commands, flags, and arguments +- **`dumpbin` / PE inspection** — extract exported function names and signatures from Windows DLLs for native library integration +- **.NET reflection** — load a managed assembly and enumerate public types, methods, and parameters via `System.Reflection` +- **Man pages** — parse `man` output for POSIX CLI tools +- **Python `inspect` / `pydoc`** — introspect Python modules and their docstrings +- **GraphQL introspection** — query a GraphQL endpoint's introspection schema to enumerate types and operations +- **gRPC / Protobuf** — parse `.proto` files or use server reflection to enumerate services and RPC methods + +Each new crawler should implement the same `DiscoveryResult` contract so downstream phases (phrase gen, schema gen) remain crawler-agnostic. + ## Architecture See [AGENTS.md](./AGENTS.md) for details on the agent structure, how to extend it, and how each phase's LLM prompting works. From 97ce05df0a86055e7d57cd1f0a350807532f2032 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Mon, 13 Apr 2026 17:21:05 -0700 Subject: [PATCH 25/33] Updated with agent patterns --- ts/docs/architecture/agent-patterns.md | 321 +++++++++ ts/packages/agents/onboarding/AGENTS.md | 45 ++ ts/packages/agents/onboarding/README.md | 18 +- .../src/scaffolder/scaffolderHandler.ts | 629 +++++++++++++++++- .../src/scaffolder/scaffolderSchema.agr | 52 +- .../src/scaffolder/scaffolderSchema.ts | 22 +- 6 files changed, 1081 insertions(+), 6 deletions(-) create mode 100644 ts/docs/architecture/agent-patterns.md diff --git a/ts/docs/architecture/agent-patterns.md b/ts/docs/architecture/agent-patterns.md new file mode 100644 index 0000000000..980a3e7705 --- /dev/null +++ b/ts/docs/architecture/agent-patterns.md @@ -0,0 +1,321 @@ +# Agent Patterns — Architecture & Design + +> **Scope:** This document describes the nine architectural patterns used by +> TypeAgent application agents. Use it when designing a new agent or choosing +> a scaffolding template. For the dispatcher that hosts agents, see +> `dispatcher.md`. For how to automate agent creation, see the onboarding +> agent at `packages/agents/onboarding/`. + +## Overview + +Every TypeAgent agent exports an `instantiate(): AppAgent` function and +implements the `AppAgent` interface from `@typeagent/agent-sdk`. Beyond that +common contract, agents fall into nine patterns based on how they communicate +with external systems, whether they manage persistent state, and what kind of +output they produce. + +| Pattern | When to use | Examples | +| ------------------------ | ---------------------------------------- | ------------------------------- | +| `schema-grammar` | Bounded set of typed actions (default) | `weather`, `photo`, `list` | +| `external-api` | Authenticated REST / cloud API | `calendar`, `email`, `player` | +| `llm-streaming` | Agent calls an LLM, streams results | `chat`, `greeting` | +| `sub-agent-orchestrator` | API surface too large for one schema | `desktop`, `code`, `browser` | +| `websocket-bridge` | Automate a host app via a plugin | `browser`, `code` | +| `state-machine` | Multi-phase workflow with approval gates | `onboarding`, `scriptflow` | +| `native-platform` | OS / device APIs, no cloud | `androidMobile`, `playerLocal` | +| `view-ui` | Rich interactive web-view UI | `turtle`, `montage`, `markdown` | +| `command-handler` | Simple settings-style direct dispatch | `settings`, `test` | + +--- + +## Pattern details + +### 1. `schema-grammar` — Standard (default) + +The canonical TypeAgent pattern. Define TypeScript action types, generate a +`.agr` grammar file for natural language matching, and implement a typed +dispatch handler. + +**File layout** + +``` +src/ + Manifest.json ← agent metadata, schema pointers + Schema.ts ← exported union of action types + Schema.agr ← grammar rules (NL → action) + ActionHandler.ts ← instantiate(); executeAction() dispatch +``` + +**AppAgent lifecycle** + +```typescript +export function instantiate(): AppAgent { + return { initializeAgentContext, executeAction }; +} +``` + +**When to choose:** any integration with a well-defined, enumerable set of +actions — REST APIs, CLI tools, file operations, data queries. + +**Examples:** `weather`, `photo`, `list`, `image`, `video` + +--- + +### 2. `external-api` — REST / OAuth Bridge + +Extends the standard pattern with an API client class and token persistence. +The handler creates a client on `initializeAgentContext` and authenticates +lazily or eagerly on `updateAgentContext`. + +**Additional files** + +``` +src/ + Bridge.ts ← API client class with auth + HTTP methods +~/.typeagent/profiles///token.json ← persisted OAuth token +``` + +**Manifest flags:** none beyond standard. + +**When to choose:** cloud services requiring OAuth or API-key auth — MS Graph, +Spotify, GitHub, Slack, etc. + +**Examples:** `calendar` (MS Graph), `email` (MS Graph + Google), `player` +(Spotify) + +--- + +### 3. `llm-streaming` — LLM-Injected / Streaming + +Runs inside the dispatcher process rather than as a sandboxed plugin +(`injected: true`). The handler calls an LLM directly and streams partial +results back to the client via `streamingActionContext`. + +**Manifest flags** + +```json +{ + "injected": true, + "cached": false, + "streamingActions": ["generateResponse"] +} +``` + +**Dependencies added:** `aiclient`, `typechat` + +**When to choose:** conversational or generative agents that need to produce +streaming text — chat assistants, summarizers, code generators. + +**Examples:** `chat`, `greeting` + +--- + +### 4. `sub-agent-orchestrator` — Multiple Sub-Schemas + +A root agent with a `subActionManifests` map in its manifest. Each sub-schema +has its own TypeScript types, grammar file, and handler module. The root +`executeAction` routes to the appropriate module based on action name (each +sub-schema owns a disjoint set of names). + +**File layout** + +``` +src/ + Manifest.json ← includes subActionManifests map + Schema.ts ← root union type (optional) + ActionHandler.ts ← routes to sub-handlers + actions/ + ActionsSchema.ts ← per-group action types + ActionsSchema.agr ← per-group grammar +``` + +**Manifest structure** + +```json +{ + "subActionManifests": { + "groupOne": { "schema": { ... } }, + "groupTwo": { "schema": { ... } } + } +} +``` + +**When to choose:** integrations whose API surface spans distinct domains that +would make a single schema unwieldy — editor + debugger + terminal, or +read/write/admin operations. + +**Examples:** `desktop` (7 sub-agents), `code` (6), `browser` (4), `onboarding` (7) + +--- + +### 5. `websocket-bridge` — Host Plugin via WebSocket + +The TypeAgent handler owns a `WebSocketServer`. A host-side plugin (VS Code +extension, browser extension, Electron renderer, Office add-in) connects as +the WebSocket client. Commands flow TypeAgent → WebSocket → plugin; results +flow back. Requires a companion plugin project. + +**File layout** + +``` +src/ + ActionHandler.ts ← starts WebSocketServer, forwards actions + Bridge.ts ← WebSocket server + pending-request map +plugin/ (or extension/) + ← connects to the bridge and calls host APIs +``` + +**AppAgent lifecycle:** implements `closeAgentContext()` to stop the server. + +**Dependencies added:** `ws` + +**When to choose:** automating an application that runs its own JS/TS runtime +(VS Code, Electron, browser, Office). + +**Examples:** `browser`, `code` + +--- + +### 6. `state-machine` — Multi-Phase Workflow + +Persists phase state to `~/.typeagent///state.json`. Each +phase progresses `pending → in-progress → approved` and must be approved +before the next phase begins. Designed for long-running automation that spans +multiple sessions. + +**State structure** + +```typescript +type WorkflowState = { + workflowId: string; + currentPhase: string; + phases: Record; + config: Record; + createdAt: string; + updatedAt: string; +}; +``` + +**When to choose:** build pipelines, onboarding flows, multi-step test +suites — any workflow where a human must review and approve each stage before +proceeding. + +**Examples:** `onboarding`, `scriptflow`, `taskflow` + +--- + +### 7. `native-platform` — OS / Device Automation + +Invokes platform APIs directly via `child_process.exec` / `spawn`, device +SDKs, or signal handling. No cloud dependency. + +**Key considerations** + +- Branch on `process.platform` (`"win32"` / `"darwin"` / `"linux"`) for + cross-platform commands. +- Use `SIGSTOP` / `SIGCONT` for pause/resume on Unix where applicable. +- Keep side effects narrow — prefer reversible commands. + +**When to choose:** controlling a desktop application, mobile device, or +system service that exposes no REST API. + +**Examples:** `androidMobile`, `playerLocal` (macOS `afplay` / Linux `mpv`), +`desktop` + +--- + +### 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. + +**File layout** + +``` +src/ + ActionHandler.ts ← opens/closes view, handles actions + ipcTypes.ts ← shared message types for handler ↔ view IPC +site/ + index.html ← web view entry point + ... +``` + +**Manifest flags:** `"localView": true` + +**When to choose:** agents that need a rich interactive UI beyond simple text +or markdown output. + +**Examples:** `turtle`, `montage`, `markdown` + +--- + +### 9. `command-handler` — Direct Dispatch + +Uses a `handlers` map keyed by action name instead of the typed `executeAction` +pipeline. Actions map directly to named handler functions. The pattern is +suited for agents with a small number of well-known, settings-style commands +where the full schema + grammar machinery adds more overhead than value. + +```typescript +export function instantiate(): AppAgent { + return getCommandInterface(handlers); +} + +const handlers = { + setSetting: async (params) => { + /* ... */ + }, + getSetting: async (params) => { + /* ... */ + }, +}; +``` + +**When to choose:** configuration agents, toggle-style controls, admin tools. + +**Examples:** `settings`, `test` + +--- + +## Choosing a pattern + +``` +Does the agent need to stream text from an LLM? + └─ Yes → llm-streaming + +Does the agent automate an app with its own JS/TS runtime? + └─ Yes → websocket-bridge + +Does the agent span a multi-step, human-gated workflow? + └─ Yes → state-machine + +Is the API surface too large for one schema? + └─ Yes → sub-agent-orchestrator + +Does the agent need a rich interactive UI? + └─ Yes → view-ui + +Does the agent call an authenticated cloud API? + └─ Yes → external-api + +Does the agent invoke OS/device APIs directly? + └─ Yes → native-platform + +Does the agent have only a handful of well-known commands? + └─ Yes → command-handler + +Otherwise → schema-grammar (default) +``` + +## Scaffolding + +The onboarding agent's scaffolder can generate boilerplate for any pattern: + +``` +scaffold the agent using the pattern +``` + +Or use `list agent patterns` at runtime for the full table. See +`packages/agents/onboarding/` for details. diff --git a/ts/packages/agents/onboarding/AGENTS.md b/ts/packages/agents/onboarding/AGENTS.md index b94265d4f6..1b8da5da67 100644 --- a/ts/packages/agents/onboarding/AGENTS.md +++ b/ts/packages/agents/onboarding/AGENTS.md @@ -71,6 +71,51 @@ Each phase has a status: `pending → in-progress → approved`. An `approve*` a - `aiclient` — `createChatModelDefault`, `ChatModel` - `typechat` — `createJsonTranslator` for structured LLM output +## Scaffolder — choosing a pattern + +The scaffolder (Phase 5) generates pattern-appropriate boilerplate. Before calling `scaffoldAgent`, determine which pattern fits the integration being onboarded. The discovery phase artifacts should give you enough information to decide. + +**Decision guide** + +| Signal from discovery | Pattern to use | +| ------------------------------------------------------------------------ | -------------------------- | +| Integration streams text (chat, code gen, summarization) | `llm-streaming` | +| Integration is a desktop/browser/Electron app with a JS runtime | `websocket-bridge` | +| Integration is a long-running, multi-step process needing human sign-off | `state-machine` | +| API surface has 5+ distinct domains (e.g., files + calendar + mail) | `sub-agent-orchestrator` | +| Integration needs a custom interactive UI | `view-ui` | +| Integration has an authenticated REST or OAuth API | `external-api` | +| Integration is a CLI tool, mobile device, or OS service | `native-platform` | +| Integration has only a few toggle/config actions | `command-handler` | +| None of the above | `schema-grammar` (default) | + +**Scaffold with a pattern** + +``` +scaffold the agent using the pattern +``` + +**List all patterns** + +``` +list agent patterns +``` + +Full pattern reference (file layouts, manifest flags, example code) is in +[docs/architecture/agent-patterns.md](../../../../docs/architecture/agent-patterns.md). + +**What the scaffolder generates per pattern** + +- `schema-grammar` — manifest, handler, schema, grammar, tsconfigs, package.json +- `external-api` — above + `*Bridge.ts` with an API client class stub; adds `aiclient` dependency +- `llm-streaming` — above + `injected: true / cached: false / streamingActions` in manifest; adds `aiclient` + `typechat` dependencies +- `sub-agent-orchestrator` — above + `actions/` directory with per-group schema and grammar stubs; `subActionManifests` in manifest +- `websocket-bridge` — above + `*Bridge.ts` with a `WebSocketServer` + pending-request map; adds `ws` dependency +- `state-machine` — above + state type definitions and `loadState` / `saveState` helpers +- `native-platform` — above + `child_process` / platform-branching boilerplate +- `view-ui` — above + `openLocalView` / `closeLocalView` lifecycle; `localView: true` in manifest +- `command-handler` — replaces `executeAction` dispatch with a named `handlers` map + ## Testing Run phrase→action tests with the `runTests` action after completing the testing phase setup. Results are saved to `~/.typeagent/onboarding//testing/results.json`. The `proposeRepair` action uses an LLM to suggest schema/grammar fixes for failing tests. diff --git a/ts/packages/agents/onboarding/README.md b/ts/packages/agents/onboarding/README.md index 57306a3b94..e0d87c92d2 100644 --- a/ts/packages/agents/onboarding/README.md +++ b/ts/packages/agents/onboarding/README.md @@ -107,7 +107,23 @@ TypeAgent exposes a **Command Executor MCP server** that bridges any MCP-compati 4. **Restart your AI client** (Claude Code or Copilot) to pick up the new MCP configuration. -Once connected, your AI client can drive onboarding phases end-to-end using natural language — e.g. *"start onboarding for Slack"* — without any manual copy-paste between tools. +Once connected, your AI client can drive onboarding phases end-to-end using natural language — e.g. _"start onboarding for Slack"_ — without any manual copy-paste between tools. + +## Agent Patterns + +The scaffolder supports nine architectural patterns. Use `list agent patterns` at runtime for the full table, or see [docs/architecture/agent-patterns.md](../../../../docs/architecture/agent-patterns.md) for the complete reference including when-to-use guidance, file layouts, and manifest flags. + +| Pattern | When to use | Examples | +| ------------------------ | ---------------------------------------- | ------------------------------- | +| `schema-grammar` | Bounded set of typed actions (default) | `weather`, `photo`, `list` | +| `external-api` | Authenticated REST / cloud API | `calendar`, `email`, `player` | +| `llm-streaming` | Agent calls an LLM, streams results | `chat`, `greeting` | +| `sub-agent-orchestrator` | API surface too large for one schema | `desktop`, `code`, `browser` | +| `websocket-bridge` | Automate a host app via a plugin | `browser`, `code` | +| `state-machine` | Multi-phase workflow with approval gates | `onboarding`, `scriptflow` | +| `native-platform` | OS / device APIs, no cloud | `androidMobile`, `playerLocal` | +| `view-ui` | Rich interactive web-view UI | `turtle`, `montage`, `markdown` | +| `command-handler` | Simple settings-style direct dispatch | `settings`, `test` | ## Building diff --git a/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts b/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts index 9c834f903a..96bb718f72 100644 --- a/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts +++ b/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts @@ -11,7 +11,7 @@ import { ActionResult, } from "@typeagent/agent-sdk"; import { createActionResultFromMarkdownDisplay } from "@typeagent/agent-sdk/helpers/action"; -import { ScaffolderActions } from "./scaffolderSchema.js"; +import { AgentPattern, ScaffolderActions } from "./scaffolderSchema.js"; import { loadState, updatePhase, @@ -49,6 +49,7 @@ export async function executeScaffolderAction( case "scaffoldAgent": return handleScaffoldAgent( action.parameters.integrationName, + action.parameters.pattern, action.parameters.outputDir, ); case "scaffoldPlugin": @@ -59,11 +60,14 @@ export async function executeScaffolderAction( ); case "listTemplates": return handleListTemplates(); + case "listPatterns": + return handleListPatterns(); } } async function handleScaffoldAgent( integrationName: string, + pattern: AgentPattern = "schema-grammar", outputDir?: string, ): Promise { const state = await loadState(integrationName); @@ -173,6 +177,7 @@ async function handleScaffoldAgent( integrationName, pascalName, state.config.description ?? "", + pattern, subGroups, ), null, @@ -184,7 +189,7 @@ async function handleScaffoldAgent( // Stamp out handler await writeFile( path.join(srcDir, `${integrationName}ActionHandler.ts`), - buildHandler(integrationName, pascalName), + buildHandler(integrationName, pascalName, pattern), ); files.push(`src/${integrationName}ActionHandler.ts`); @@ -197,6 +202,7 @@ async function handleScaffoldAgent( integrationName, packageName, pascalName, + pattern, subSchemaNames, ), null, @@ -404,6 +410,7 @@ function buildManifest( name: string, pascalName: string, description: string, + pattern: AgentPattern = "schema-grammar", subGroups?: SubSchemaGroup[], ) { const manifest: Record = { @@ -419,6 +426,15 @@ function buildManifest( }, }; + // Pattern-specific manifest flags + if (pattern === "llm-streaming") { + manifest.injected = true; + manifest.cached = false; + manifest.streamingActions = ["generateResponse"]; + } else if (pattern === "view-ui") { + manifest.localView = true; + } + if (subGroups && subGroups.length > 0) { const subActionManifests: Record = {}; for (const group of subGroups) { @@ -439,7 +455,34 @@ function buildManifest( return manifest; } -function buildHandler(name: string, pascalName: string): string { +function buildHandler( + name: string, + pascalName: string, + pattern: AgentPattern = "schema-grammar", +): string { + switch (pattern) { + case "external-api": + return buildExternalApiHandler(name, pascalName); + case "llm-streaming": + return buildLlmStreamingHandler(name, pascalName); + case "sub-agent-orchestrator": + return buildSubAgentOrchestratorHandler(name, pascalName); + case "websocket-bridge": + return buildWebSocketBridgeHandler(name, pascalName); + case "state-machine": + return buildStateMachineHandler(name, pascalName); + case "native-platform": + return buildNativePlatformHandler(name, pascalName); + case "view-ui": + return buildViewUiHandler(name, pascalName); + case "command-handler": + return buildCommandHandlerTemplate(name, pascalName); + default: + return buildSchemaGrammarHandler(name, pascalName); + } +} + +function buildSchemaGrammarHandler(name: string, pascalName: string): string { return `// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. @@ -479,6 +522,7 @@ function buildPackageJson( name: string, packageName: string, pascalName: string, + pattern: AgentPattern = "schema-grammar", subSchemaNames?: string[], ) { const scripts: Record = { @@ -518,6 +562,13 @@ function buildPackageJson( scripts, dependencies: { "@typeagent/agent-sdk": "workspace:*", + ...(pattern === "llm-streaming" + ? { aiclient: "workspace:*", typechat: "workspace:*" } + : pattern === "external-api" + ? { aiclient: "workspace:*" } + : pattern === "websocket-bridge" + ? { ws: "^8.18.0" } + : {}), }, devDependencies: { "@typeagent/action-schema-compiler": "workspace:*", @@ -747,3 +798,575 @@ async function writeFile(filePath: string, content: string): Promise { await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, content, "utf-8"); } + +// ─── Pattern listing ───────────────────────────────────────────────────────── + +async function handleListPatterns(): Promise { + const lines = [ + `## Agent architectural patterns`, + ``, + `Pass \`pattern\` to \`scaffoldAgent\` to generate pattern-appropriate boilerplate.`, + ``, + `| Pattern | When to use | Examples |`, + `|---------|-------------|----------|`, + `| \`schema-grammar\` | Standard: bounded set of typed actions (default) | weather, photo, list |`, + `| \`external-api\` | REST/OAuth cloud API (MS Graph, Spotify, GitHub…) | calendar, email, player |`, + `| \`llm-streaming\` | Agent calls an LLM and streams partial results | chat, greeting |`, + `| \`sub-agent-orchestrator\` | API surface too large for one schema; split into groups | desktop, code, browser |`, + `| \`websocket-bridge\` | Automate an app via a host-side plugin over WebSocket | browser, code |`, + `| \`state-machine\` | Multi-phase workflow with approval gates and disk persistence | onboarding, scriptflow |`, + `| \`native-platform\` | OS/device APIs via child_process or SDK; no cloud | androidMobile, playerLocal |`, + `| \`view-ui\` | Rich interactive UI rendered in a local web view | turtle, montage, markdown |`, + `| \`command-handler\` | Simple settings-style agent; direct dispatch, no schema | settings, test |`, + ]; + return createActionResultFromMarkdownDisplay(lines.join("\n")); +} + +// ─── 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, + 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: ActionContext, +): Promise { + // Optionally authenticate eagerly when the agent is enabled. +} + +async function executeAction( + action: TypeAgentAction<${pascalName}Actions>, + context: ActionContext, +): Promise { + const { client } = context.agentContext; + // TODO: map each action to a client.callApi() call. + return createActionResultFromTextDisplay( + \`Executing \${action.actionName} — not yet implemented.\`, + ); +} +`; +} + +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}\`, + ); + } +} +`; +} + +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 { ... } +`; +} + +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, + 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: ActionContext, +): Promise {} + +async function closeAgentContext(context: ActionContext): Promise { + await context.agentContext.bridge.stop(); +} + +async function executeAction( + action: TypeAgentAction<${pascalName}Actions>, + context: ActionContext, +): Promise { + const { bridge } = context.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) }; + } +} +`; +} + +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.\`, + ); +} +`; +} + +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}\`); + } +} +`; +} + +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, + 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: ActionContext, +): Promise { + if (enable) { + await context.sessionContext.agentIO.openLocalView( + context.sessionContext.requestId, + VIEW_PORT, + ); + } else { + await context.sessionContext.agentIO.closeLocalView( + context.sessionContext.requestId, + VIEW_PORT, + ); + } +} + +async function closeAgentContext(_context: ActionContext): 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.

\`, + ); +} +`; +} + +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); + }, + }; +} +`; +} diff --git a/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.agr b/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.agr index a7752c25f5..c355076f59 100644 --- a/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.agr +++ b/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.agr @@ -14,6 +14,42 @@ parameters: { integrationName } +} + | scaffold (the)? $(integrationName:wildcard) agent (using | with | as) (a | the)? schema grammar pattern -> { + actionName: "scaffoldAgent", + parameters: { integrationName, pattern: "schema-grammar" } +} + | scaffold (the)? $(integrationName:wildcard) agent (using | with | as) (a | the)? external api pattern -> { + actionName: "scaffoldAgent", + parameters: { integrationName, pattern: "external-api" } +} + | scaffold (the)? $(integrationName:wildcard) agent (using | with | as) (a | the)? (llm)? streaming pattern -> { + actionName: "scaffoldAgent", + parameters: { integrationName, pattern: "llm-streaming" } +} + | scaffold (the)? $(integrationName:wildcard) agent (using | with | as) (a | the)? (sub agent)? orchestrator pattern -> { + actionName: "scaffoldAgent", + parameters: { integrationName, pattern: "sub-agent-orchestrator" } +} + | scaffold (the)? $(integrationName:wildcard) agent (using | with | as) (a | the)? websocket (bridge)? pattern -> { + actionName: "scaffoldAgent", + parameters: { integrationName, pattern: "websocket-bridge" } +} + | scaffold (the)? $(integrationName:wildcard) agent (using | with | as) (a | the)? state machine pattern -> { + actionName: "scaffoldAgent", + parameters: { integrationName, pattern: "state-machine" } +} + | scaffold (the)? $(integrationName:wildcard) agent (using | with | as) (a | the)? native platform pattern -> { + actionName: "scaffoldAgent", + parameters: { integrationName, pattern: "native-platform" } +} + | scaffold (the)? $(integrationName:wildcard) agent (using | with | as) (a | the)? view (ui)? pattern -> { + actionName: "scaffoldAgent", + parameters: { integrationName, pattern: "view-ui" } +} + | scaffold (the)? $(integrationName:wildcard) agent (using | with | as) (a | the)? command handler pattern -> { + actionName: "scaffoldAgent", + parameters: { integrationName, pattern: "command-handler" } }; = scaffold (the)? $(integrationName:wildcard) (plugin | extension) -> { @@ -40,8 +76,22 @@ parameters: {} }; + = list (available | agent)? patterns -> { + actionName: "listPatterns", + parameters: {} +} + | (show | what are) (the)? (available | agent)? (agent)? patterns -> { + actionName: "listPatterns", + parameters: {} +} + | what patterns (are)? (available | supported)? -> { + actionName: "listPatterns", + parameters: {} +}; + import { ScaffolderActions } from "./scaffolderSchema.ts"; : ScaffolderActions = | - | ; + | + | ; diff --git a/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.ts b/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.ts index 3dedf17ea8..6de7584c34 100644 --- a/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.ts +++ b/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.ts @@ -1,16 +1,31 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +// Agent architectural patterns supported by the scaffolder. +export type AgentPattern = + | "schema-grammar" // Standard: schema + grammar + dispatch handler (default) + | "external-api" // REST/OAuth cloud API bridge (MS Graph, Spotify, etc.) + | "llm-streaming" // LLM-injected agent with streaming responses + | "sub-agent-orchestrator" // Root agent routing to N typed sub-schemas + | "websocket-bridge" // Bidirectional WebSocket to a host-side plugin + | "state-machine" // Multi-phase disk-persisted workflow + | "native-platform" // OS/device APIs via child_process or SDK + | "view-ui" // Web view renderer with IPC handler + | "command-handler"; // CommandHandler (direct dispatch, no typed schema) + export type ScaffolderActions = | ScaffoldAgentAction | ScaffoldPluginAction - | ListTemplatesAction; + | ListTemplatesAction + | ListPatternsAction; export type ScaffoldAgentAction = { actionName: "scaffoldAgent"; parameters: { // Integration name to scaffold agent for integrationName: string; + // Architectural pattern to use (defaults to "schema-grammar") + pattern?: AgentPattern; // Target directory for the agent package (defaults to ts/packages/agents/) outputDir?: string; }; @@ -37,3 +52,8 @@ export type ListTemplatesAction = { actionName: "listTemplates"; parameters: {}; }; + +export type ListPatternsAction = { + actionName: "listPatterns"; + parameters: {}; +}; From f69a0be9f5619ea577ecd54b073d998444b5ead6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 00:28:28 +0000 Subject: [PATCH 26/33] Initial plan From 61fc5ba0d4792bc9cf0f4696a3ab1fcbc9e9b490 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 00:34:01 +0000 Subject: [PATCH 27/33] Fix Repo Policy Check: Add Trademark section to onboarding README and sort package.json Agent-Logs-Url: https://github.com/microsoft/TypeAgent/sessions/038f7f2f-4aba-4463-9150-ef9e660bdd4d Co-authored-by: robgruen <25374553+robgruen@users.noreply.github.com> --- ts/packages/agents/onboarding/AGENTS.md | 121 ++ ts/packages/agents/onboarding/README.md | 161 ++ ts/packages/agents/onboarding/USER_GUIDE.md | 532 +++++++ ts/packages/agents/onboarding/package.json | 61 + .../src/discovery/discoveryHandler.ts | 641 ++++++++ .../src/discovery/discoverySchema.agr | 71 + .../src/discovery/discoverySchema.ts | 50 + .../src/grammarGen/grammarGenHandler.ts | 297 ++++ .../src/grammarGen/grammarGenSchema.agr | 49 + .../src/grammarGen/grammarGenSchema.ts | 31 + ts/packages/agents/onboarding/src/lib/llm.ts | 34 + .../agents/onboarding/src/lib/workspace.ts | 214 +++ .../onboarding/src/onboardingActionHandler.ts | 290 ++++ .../onboarding/src/onboardingManifest.json | 77 + .../onboarding/src/onboardingSchema.agr | 79 + .../agents/onboarding/src/onboardingSchema.ts | 54 + .../src/packaging/packagingHandler.ts | 602 ++++++++ .../src/packaging/packagingSchema.agr | 63 + .../src/packaging/packagingSchema.ts | 46 + .../src/phraseGen/phraseGenHandler.ts | 299 ++++ .../src/phraseGen/phraseGenSchema.agr | 66 + .../src/phraseGen/phraseGenSchema.ts | 52 + .../src/scaffolder/scaffolderHandler.ts | 1372 +++++++++++++++++ .../src/scaffolder/scaffolderSchema.agr | 97 ++ .../src/scaffolder/scaffolderSchema.ts | 59 + .../src/schemaGen/schemaGenHandler.ts | 278 ++++ .../src/schemaGen/schemaGenSchema.agr | 54 + .../src/schemaGen/schemaGenSchema.ts | 34 + .../onboarding/src/testing/testingHandler.ts | 649 ++++++++ .../onboarding/src/testing/testingSchema.agr | 77 + .../onboarding/src/testing/testingSchema.ts | 57 + .../agents/onboarding/src/tsconfig.json | 12 + ts/packages/agents/onboarding/tsconfig.json | 11 + .../defaultAgentProvider/data/config.json | 3 + ts/pnpm-lock.yaml | 43 + 35 files changed, 6636 insertions(+) create mode 100644 ts/packages/agents/onboarding/AGENTS.md create mode 100644 ts/packages/agents/onboarding/README.md create mode 100644 ts/packages/agents/onboarding/USER_GUIDE.md create mode 100644 ts/packages/agents/onboarding/package.json create mode 100644 ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts create mode 100644 ts/packages/agents/onboarding/src/discovery/discoverySchema.agr create mode 100644 ts/packages/agents/onboarding/src/discovery/discoverySchema.ts create mode 100644 ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts create mode 100644 ts/packages/agents/onboarding/src/grammarGen/grammarGenSchema.agr create mode 100644 ts/packages/agents/onboarding/src/grammarGen/grammarGenSchema.ts create mode 100644 ts/packages/agents/onboarding/src/lib/llm.ts create mode 100644 ts/packages/agents/onboarding/src/lib/workspace.ts create mode 100644 ts/packages/agents/onboarding/src/onboardingActionHandler.ts create mode 100644 ts/packages/agents/onboarding/src/onboardingManifest.json create mode 100644 ts/packages/agents/onboarding/src/onboardingSchema.agr create mode 100644 ts/packages/agents/onboarding/src/onboardingSchema.ts create mode 100644 ts/packages/agents/onboarding/src/packaging/packagingHandler.ts create mode 100644 ts/packages/agents/onboarding/src/packaging/packagingSchema.agr create mode 100644 ts/packages/agents/onboarding/src/packaging/packagingSchema.ts create mode 100644 ts/packages/agents/onboarding/src/phraseGen/phraseGenHandler.ts create mode 100644 ts/packages/agents/onboarding/src/phraseGen/phraseGenSchema.agr create mode 100644 ts/packages/agents/onboarding/src/phraseGen/phraseGenSchema.ts create mode 100644 ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts create mode 100644 ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.agr create mode 100644 ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.ts create mode 100644 ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts create mode 100644 ts/packages/agents/onboarding/src/schemaGen/schemaGenSchema.agr create mode 100644 ts/packages/agents/onboarding/src/schemaGen/schemaGenSchema.ts create mode 100644 ts/packages/agents/onboarding/src/testing/testingHandler.ts create mode 100644 ts/packages/agents/onboarding/src/testing/testingSchema.agr create mode 100644 ts/packages/agents/onboarding/src/testing/testingSchema.ts create mode 100644 ts/packages/agents/onboarding/src/tsconfig.json create mode 100644 ts/packages/agents/onboarding/tsconfig.json diff --git a/ts/packages/agents/onboarding/AGENTS.md b/ts/packages/agents/onboarding/AGENTS.md new file mode 100644 index 0000000000..1b8da5da67 --- /dev/null +++ b/ts/packages/agents/onboarding/AGENTS.md @@ -0,0 +1,121 @@ +# AGENTS.md — Onboarding Agent + +This document is for AI agents (Claude Code, GitHub Copilot, etc.) working with the onboarding agent codebase. + +## What this agent does + +The onboarding agent automates integrating a new application or API into TypeAgent. It is itself a TypeAgent agent, so its actions are available to AI orchestrators via TypeAgent's MCP interface using `list_commands`. + +## Agent structure + +``` +src/ + onboardingManifest.json ← main manifest, declares 7 sub-action manifests + onboardingSchema.ts ← top-level coordination actions + onboardingSchema.agr ← grammar for top-level actions + onboardingActionHandler.ts ← instantiate(); routes all actions to phase handlers + lib/ + workspace.ts ← read/write per-integration state on disk + llm.ts ← aiclient ChatModel factories per phase + discovery/ ← Phase 1: API surface enumeration + phraseGen/ ← Phase 2: natural language phrase generation + schemaGen/ ← Phase 3: TypeScript action schema generation + grammarGen/ ← Phase 4: .agr grammar generation + scaffolder/ ← Phase 5: agent package scaffolding + testing/ ← Phase 6: phrase→action test loop + packaging/ ← Phase 7: packaging and distribution +``` + +## How actions are routed + +`onboardingActionHandler.ts` exports `instantiate()` which returns a single `AppAgent`. The `executeAction` method receives all actions (from main schema and all sub-schemas) and dispatches by `action.actionName` to the appropriate phase handler module. + +## Workspace state + +All artifacts are persisted at `~/.typeagent/onboarding//`. The `workspace.ts` lib provides: + +- `createWorkspace(config)` — initialize a new integration workspace +- `loadState(name)` — load current phase state +- `saveState(state)` — persist state +- `updatePhase(name, phase, update)` — update phase status; automatically advances `currentPhase` on approval +- `readArtifact(name, phase, filename)` — read a phase artifact +- `writeArtifact(name, phase, filename, content)` — write a phase artifact +- `listIntegrations()` — list all integration workspaces + +## LLM usage + +Each phase that requires LLM calls uses `aiclient`'s `createChatModelDefault(tag)`. Tags are namespaced as `onboarding:` (e.g. `onboarding:schemagen`). This follows the standard TypeAgent pattern — credentials come from `ts/.env`. + +## Phase approval model + +Each phase has a status: `pending → in-progress → approved`. An `approve*` action locks artifacts and advances to the next phase. The AI orchestrator is expected to review artifacts before calling approve — this is the human-in-the-loop checkpoint. + +## Adding a new phase + +1. Create `src//` with `*Schema.ts`, `*Schema.agr`, `*Handler.ts` +2. Add the sub-action manifest entry to `onboardingManifest.json` +3. Add `asc:*` and `agc:*` build scripts to `package.json` +4. Import and wire up the handler in `onboardingActionHandler.ts` +5. Add the phase to the `OnboardingPhase` type and `phases` object in `workspace.ts` + +## Adding a new tool to an existing phase + +1. Add the action type to the phase's `*Schema.ts` +2. Add grammar patterns to the phase's `*Schema.agr` +3. Implement the handler case in the phase's `*Handler.ts` + +## Key dependencies + +- `@typeagent/agent-sdk` — `AppAgent`, `ActionContext`, `TypeAgentAction`, `ActionResult` +- `@typeagent/agent-sdk/helpers/action` — `createActionResultFromTextDisplay`, `createActionResultFromMarkdownDisplay` +- `aiclient` — `createChatModelDefault`, `ChatModel` +- `typechat` — `createJsonTranslator` for structured LLM output + +## Scaffolder — choosing a pattern + +The scaffolder (Phase 5) generates pattern-appropriate boilerplate. Before calling `scaffoldAgent`, determine which pattern fits the integration being onboarded. The discovery phase artifacts should give you enough information to decide. + +**Decision guide** + +| Signal from discovery | Pattern to use | +| ------------------------------------------------------------------------ | -------------------------- | +| Integration streams text (chat, code gen, summarization) | `llm-streaming` | +| Integration is a desktop/browser/Electron app with a JS runtime | `websocket-bridge` | +| Integration is a long-running, multi-step process needing human sign-off | `state-machine` | +| API surface has 5+ distinct domains (e.g., files + calendar + mail) | `sub-agent-orchestrator` | +| Integration needs a custom interactive UI | `view-ui` | +| Integration has an authenticated REST or OAuth API | `external-api` | +| Integration is a CLI tool, mobile device, or OS service | `native-platform` | +| Integration has only a few toggle/config actions | `command-handler` | +| None of the above | `schema-grammar` (default) | + +**Scaffold with a pattern** + +``` +scaffold the agent using the pattern +``` + +**List all patterns** + +``` +list agent patterns +``` + +Full pattern reference (file layouts, manifest flags, example code) is in +[docs/architecture/agent-patterns.md](../../../../docs/architecture/agent-patterns.md). + +**What the scaffolder generates per pattern** + +- `schema-grammar` — manifest, handler, schema, grammar, tsconfigs, package.json +- `external-api` — above + `*Bridge.ts` with an API client class stub; adds `aiclient` dependency +- `llm-streaming` — above + `injected: true / cached: false / streamingActions` in manifest; adds `aiclient` + `typechat` dependencies +- `sub-agent-orchestrator` — above + `actions/` directory with per-group schema and grammar stubs; `subActionManifests` in manifest +- `websocket-bridge` — above + `*Bridge.ts` with a `WebSocketServer` + pending-request map; adds `ws` dependency +- `state-machine` — above + state type definitions and `loadState` / `saveState` helpers +- `native-platform` — above + `child_process` / platform-branching boilerplate +- `view-ui` — above + `openLocalView` / `closeLocalView` lifecycle; `localView: true` in manifest +- `command-handler` — replaces `executeAction` dispatch with a named `handlers` map + +## Testing + +Run phrase→action tests with the `runTests` action after completing the testing phase setup. Results are saved to `~/.typeagent/onboarding//testing/results.json`. The `proposeRepair` action uses an LLM to suggest schema/grammar fixes for failing tests. diff --git a/ts/packages/agents/onboarding/README.md b/ts/packages/agents/onboarding/README.md new file mode 100644 index 0000000000..1cd09843a4 --- /dev/null +++ b/ts/packages/agents/onboarding/README.md @@ -0,0 +1,161 @@ +# Onboarding Agent + +A TypeAgent agent that automates the end-to-end process of integrating a new application or API into TypeAgent. Each phase of the onboarding pipeline is a sub-agent with typed actions, enabling AI orchestrators (Claude Code, GitHub Copilot) to drive the process via TypeAgent's MCP interface. + +## Overview + +Integrating a new application into TypeAgent involves 7 phases: + +| Phase | Sub-agent | What it does | +| ----- | ----------------------- | ------------------------------------------------------------------ | +| 1 | `onboarding-discovery` | Crawls docs or parses an OpenAPI spec to enumerate the API surface | +| 2 | `onboarding-phrasegen` | Generates natural language sample phrases for each action | +| 3 | `onboarding-schemagen` | Generates TypeScript action schemas from the API surface | +| 4 | `onboarding-grammargen` | Generates `.agr` grammar files from schemas and phrases | +| 5 | `onboarding-scaffolder` | Stamps out the agent package infrastructure | +| 6 | `onboarding-testing` | Generates test cases and runs a phrase→action validation loop | +| 7 | `onboarding-packaging` | Packages the agent for distribution and registration | + +Each phase produces **artifacts saved to disk** at `~/.typeagent/onboarding//`, so work can be resumed across sessions. + +## Usage + +### Starting a new integration + +``` +start onboarding for slack +``` + +### Checking status + +``` +what's the status of the slack onboarding +``` + +### Resuming an in-progress integration + +``` +resume onboarding for slack +``` + +### Running a specific phase + +``` +crawl docs at https://api.slack.com/docs for slack +generate phrases for slack +generate schema for slack +run tests for slack +``` + +## Workspace layout + +``` +~/.typeagent/onboarding/ + / + state.json ← phase status, config, timestamps + discovery/ + api-surface.json ← enumerated actions from docs/spec + phraseGen/ + phrases.json ← sample phrases per action + schemaGen/ + schema.ts ← generated TypeScript action schema + grammarGen/ + schema.agr ← generated grammar file + scaffolder/ + agent/ ← stamped-out agent package files + testing/ + test-cases.json ← phrase → expected action test pairs + results.json ← latest test run results + packaging/ + dist/ ← final packaged output +``` + +Each phase must be **approved** before the next phase begins. Approval locks the phase's artifacts and advances the current phase pointer in `state.json`. + +## For Best Results + +The onboarding agent is designed to be driven by an AI orchestrator (Claude Code, GitHub Copilot) that can call TypeAgent actions iteratively, inspect artifacts, and guide each phase to completion. For the best experience, set up TypeAgent as an MCP server so your AI client can communicate with it directly. + +### Set up TypeAgent as an MCP server + +TypeAgent exposes a **Command Executor MCP server** that bridges any MCP-compatible client (Claude Code, GitHub Copilot) to the TypeAgent dispatcher. Full setup instructions are in [packages/commandExecutor/README.md](../../commandExecutor/README.md). The short version: + +1. **Build** the workspace (from `ts/`): + + ```bash + pnpm run build + ``` + +2. **Add the MCP server** to `.mcp.json` at the repo root (create it if it doesn't exist): + + ```json + { + "mcpServers": { + "command-executor": { + "command": "node", + "args": ["packages/commandExecutor/dist/server.js"] + } + } + } + ``` + +3. **Start the TypeAgent dispatcher** (in a separate terminal): + + ```bash + pnpm run start:agent-server + ``` + +4. **Restart your AI client** (Claude Code or Copilot) to pick up the new MCP configuration. + +Once connected, your AI client can drive onboarding phases end-to-end using natural language — e.g. _"start onboarding for Slack"_ — without any manual copy-paste between tools. + +## Agent Patterns + +The scaffolder supports nine architectural patterns. Use `list agent patterns` at runtime for the full table, or see [docs/architecture/agent-patterns.md](../../../../docs/architecture/agent-patterns.md) for the complete reference including when-to-use guidance, file layouts, and manifest flags. + +| Pattern | When to use | Examples | +| ------------------------ | ---------------------------------------- | ------------------------------- | +| `schema-grammar` | Bounded set of typed actions (default) | `weather`, `photo`, `list` | +| `external-api` | Authenticated REST / cloud API | `calendar`, `email`, `player` | +| `llm-streaming` | Agent calls an LLM, streams results | `chat`, `greeting` | +| `sub-agent-orchestrator` | API surface too large for one schema | `desktop`, `code`, `browser` | +| `websocket-bridge` | Automate a host app via a plugin | `browser`, `code` | +| `state-machine` | Multi-phase workflow with approval gates | `onboarding`, `scriptflow` | +| `native-platform` | OS / device APIs, no cloud | `androidMobile`, `playerLocal` | +| `view-ui` | Rich interactive web-view UI | `turtle`, `montage`, `markdown` | +| `command-handler` | Simple settings-style direct dispatch | `settings`, `test` | + +## Building + +```bash +pnpm install +pnpm run build +``` + +## TODO + +### Additional discovery crawlers + +The discovery phase currently supports web docs and OpenAPI specs. Planned crawlers: + +- **CLI `--help` scraping** — invoke a command-line tool with `--help` / `--help ` and parse the output to enumerate commands, flags, and arguments +- **`dumpbin` / PE inspection** — extract exported function names and signatures from Windows DLLs for native library integration +- **.NET reflection** — load a managed assembly and enumerate public types, methods, and parameters via `System.Reflection` +- **Man pages** — parse `man` output for POSIX CLI tools +- **Python `inspect` / `pydoc`** — introspect Python modules and their docstrings +- **GraphQL introspection** — query a GraphQL endpoint's introspection schema to enumerate types and operations +- **gRPC / Protobuf** — parse `.proto` files or use server reflection to enumerate services and RPC methods + +Each new crawler should implement the same `DiscoveryResult` contract so downstream phases (phrase gen, schema gen) remain crawler-agnostic. + +## Architecture + +See [AGENTS.md](./AGENTS.md) for details on the agent structure, how to extend it, and how each phase's LLM prompting works. + +## Trademarks + +This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft +trademarks or logos is subject to and must follow +[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). +Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. +Any use of third-party trademarks or logos are subject to those third-party's policies. diff --git a/ts/packages/agents/onboarding/USER_GUIDE.md b/ts/packages/agents/onboarding/USER_GUIDE.md new file mode 100644 index 0000000000..f6146e5992 --- /dev/null +++ b/ts/packages/agents/onboarding/USER_GUIDE.md @@ -0,0 +1,532 @@ +# TypeAgent Onboarding — User Guide + +This guide shows how to use an AI assistant (Claude Code, GitHub Copilot, or any MCP client) to onboard a new application or API into TypeAgent from start to finish. + +The onboarding agent is itself a TypeAgent agent. Its actions are available in your AI assistant automatically via `discover_agents` — no extra registration required beyond the one-time MCP setup below. + +--- + +## Step 0 — Register TypeAgent as an MCP server + +Before you can use the onboarding agent from your AI assistant, you need to register TypeAgent's MCP server (`command-executor`) once. This is a one-time setup per machine. + +### What it is + +TypeAgent exposes a stdio MCP server at `ts/packages/commandExecutor/dist/server.js`. It provides three tools to your AI assistant: + +| Tool | What it does | +| ----------------- | ------------------------------------------------------------- | +| `discover_agents` | Lists all TypeAgent agents and their actions | +| `execute_action` | Calls any agent action directly by name with typed parameters | +| `execute_command` | Passes a natural language request to the TypeAgent dispatcher | + +The onboarding agent's actions (`startOnboarding`, `crawlDocUrl`, `generateSchema`, etc.) are discovered and called via these tools. + +### Prerequisites + +- Node.js ≥ 20 installed +- The TypeAgent repo cloned and built: `cd ts && pnpm install && pnpm run build` +- The TypeAgent agent-server running (started automatically on first use, or via `node packages/agentServer/server/dist/server.js` from `ts/`) +- `ts/.env` configured with your Azure OpenAI or OpenAI API keys + +--- + +### Claude Code + +Claude Code reads MCP server config from `.mcp.json` in your project root (or `~/.claude/mcp.json` for global config). + +The repo already includes `ts/.mcp.json` with the `command-executor` server. **If you open Claude Code from the `ts/` directory, it will be picked up automatically.** + +To verify it is active, run inside Claude Code: + +``` +/mcp +``` + +You should see `command-executor` listed as connected. + +**If you need to register it manually** (e.g. you're working from a different directory), add this to your `.mcp.json`: + +```json +{ + "mcpServers": { + "typeagent": { + "command": "node", + "args": [ + "/ts/packages/commandExecutor/dist/server.js" + ], + "env": {} + } + } +} +``` + +Replace `` with the full path to your TypeAgent clone, for example: + +- Windows: `C:/repos/TypeAgent/ts/packages/commandExecutor/dist/server.js` +- Mac/Linux: `/home/you/repos/TypeAgent/ts/packages/commandExecutor/dist/server.js` + +Then restart Claude Code. + +--- + +### GitHub Copilot (VS Code) + +GitHub Copilot uses VS Code's MCP configuration. Add the TypeAgent server via the VS Code settings UI or directly in `settings.json`. + +**Via settings.json** — open your VS Code `settings.json` (`Ctrl+Shift+P` → "Open User Settings (JSON)") and add: + +```json +{ + "github.copilot.chat.mcpServers": { + "typeagent": { + "command": "node", + "args": [ + "/ts/packages/commandExecutor/dist/server.js" + ], + "type": "stdio" + } + } +} +``` + +**Via the VS Code UI** — open the Command Palette (`Ctrl+Shift+P`), run **"MCP: Add MCP Server"**, choose **"Command (stdio)"**, and enter: + +- Command: `node` +- Args: `/ts/packages/commandExecutor/dist/server.js` +- Name: `typeagent` + +After saving, open a Copilot Chat panel. You should see the TypeAgent tools listed under the MCP tools icon (the plug icon in the chat input bar). + +--- + +### Verify the connection + +Once registered, ask your AI assistant: + +``` +> Discover TypeAgent agents +``` + +or + +``` +> What TypeAgent agents are available? +``` + +The assistant will call `discover_agents` and return a list that includes `onboarding` (among others). If you see the list, you're ready to start onboarding. + +**Troubleshooting:** + +- If the server isn't found, check that `ts/packages/commandExecutor/dist/server.js` exists — run `pnpm run build` from `ts/` if not +- If tools don't appear, restart your AI assistant or reload the VS Code window +- Logs are written to `~/.tmp/typeagent-mcp/` — check there for connection errors + +--- + +## Prerequisites (after MCP setup) + +- TypeAgent MCP server registered with your AI assistant (see above) +- Your `ts/.env` configured with API keys (the same ones TypeAgent already uses) +- The application you want to integrate is either documented online or has an OpenAPI spec + +--- + +## How it works + +You talk to your AI assistant in plain English. The assistant calls the onboarding agent's actions to do the work. Each phase produces artifacts saved to `~/.typeagent/onboarding//` so you can pause and come back anytime. + +``` +You (natural language) + ↓ +AI assistant (Claude Code / Copilot) + ↓ MCP → list_commands → TypeAgent +Onboarding agent actions + ↓ +Artifacts on disk (schemas, phrases, grammar, agent package) +``` + +--- + +## Complete walkthrough: onboarding a REST API + +Below is a realistic session. Lines starting with `>` are things you'd say to your AI assistant. + +--- + +### Step 1 — Start the onboarding + +``` +> Start onboarding for Slack +``` + +The assistant calls `startOnboarding` and creates a workspace at `~/.typeagent/onboarding/slack/`. + +--- + +### Step 2 — Discover the API surface + +**From documentation URL:** + +``` +> Crawl the Slack API docs at https://api.slack.com/methods for slack +``` + +**From an OpenAPI spec file:** + +``` +> Parse the OpenAPI spec at C:\specs\slack-openapi.json for slack +``` + +**From an OpenAPI spec URL:** + +``` +> Parse the OpenAPI spec at https://raw.githubusercontent.com/slackapi/slack-api-specs/master/web-api/slack_web_openapi_v2.json for slack +``` + +After crawling, review what was found: + +``` +> List the discovered actions for slack +``` + +You'll see a table of all API actions with names and descriptions. Trim down to what you actually want: + +``` +> Approve the API surface for slack, excluding: listAllUsers, adminCreateWorkspace, deleteTeam +``` + +Or include only specific actions: + +``` +> Approve the API surface for slack, including only: postMessage, listChannels, getUserInfo, addReaction, uploadFile +``` + +--- + +### Step 3 — Generate sample phrases + +``` +> Generate phrases for slack +``` + +The assistant calls `generatePhrases` and asks the LLM to produce 5 natural language samples per action. You can tune the count: + +``` +> Generate 8 phrases per action for slack +``` + +Review the output. Add or remove specific phrases: + +``` +> Add phrase "DM John about the meeting" for action postMessage in slack +> Remove phrase "send a slack" from action postMessage in slack +``` + +When satisfied: + +``` +> Approve phrases for slack +``` + +--- + +### Step 4 — Generate the TypeScript action schema + +``` +> Generate the action schema for slack +``` + +The LLM produces a TypeScript file with union types and JSDoc comments mapping each action to the Slack API. Review the output in the response. + +If you want changes: + +``` +> Refine the slack schema to make the channelId parameter optional and add a threadTs parameter to postMessage +``` + +``` +> Refine the slack schema to split postMessage into postChannelMessage and postDirectMessage +``` + +When happy: + +``` +> Approve the slack schema +``` + +--- + +### Step 5 — Generate the grammar + +``` +> Generate the grammar for slack +``` + +The LLM produces a `.agr` file with natural language patterns for each action. Then validate it compiles: + +``` +> Compile the slack grammar +``` + +If compilation fails, the error message will tell you which rule is invalid. You can ask: + +``` +> Generate the grammar for slack +``` + +again after the schema is adjusted, or manually edit the grammar file at `~/.typeagent/onboarding/slack/grammarGen/schema.agr`. + +When the grammar compiles cleanly: + +``` +> Approve the slack grammar +``` + +--- + +### Step 6 — Scaffold the agent package + +``` +> Scaffold the slack agent +``` + +This stamps out a complete TypeAgent agent package at `ts/packages/agents/slack/` with: + +- `slackManifest.json` +- `slackSchema.ts` (the approved schema) +- `slackSchema.agr` (the approved grammar) +- `slackActionHandler.ts` (stub — ready for your implementation) +- `package.json`, `tsconfig.json`, `src/tsconfig.json` + +If your integration talks to Slack over REST, scaffold the bridge too: + +``` +> Scaffold the slack rest-client plugin +``` + +For a WebSocket-based integration (like Excel or VS Code agents): + +``` +> Scaffold the slack websocket-bridge plugin +``` + +For an Office add-in: + +``` +> Scaffold the slack office-addin plugin +``` + +See what templates are available: + +``` +> List templates +``` + +--- + +### Step 7 — Package and register + +``` +> Package the slack agent +``` + +This runs `pnpm install` and `pnpm run build` in the agent directory. + +To also register it with the local TypeAgent dispatcher immediately: + +``` +> Package the slack agent and register it +``` + +Then restart TypeAgent so it picks up the new agent. + +--- + +### Step 8 — Run the tests + +After TypeAgent has restarted with the agent registered: + +``` +> Generate tests for slack +> Run tests for slack +``` + +You'll get a pass/fail table. If tests fail: + +``` +> Get the failing test results for slack +> Propose a repair for slack +``` + +The LLM analyzes the failures and suggests specific changes to the schema and/or grammar. Review the proposal, then: + +``` +> Approve the repair for slack +``` + +Then re-run: + +``` +> Run tests for slack +``` + +Repeat until pass rate is satisfactory. A common target is >90% before handing off to users. + +--- + +## Checking in on progress + +At any point: + +``` +> What's the status of the slack onboarding? +``` + +You'll see a phase-by-phase table like: + +``` +| Phase | Status | +|-------------|------------| +| discovery | ✅ approved | +| phraseGen | ✅ approved | +| schemaGen | ✅ approved | +| grammarGen | 🔄 in-progress | +| scaffolder | ⏳ pending | +| testing | ⏳ pending | +| packaging | ⏳ pending | +``` + +--- + +## Resuming an interrupted onboarding + +If you close your session and come back later: + +``` +> Resume onboarding for slack +``` + +This tells you the current phase and what to do next. All artifacts are on disk — nothing is lost. + +List all integrations in progress: + +``` +> List my integrations +> List integrations in progress +``` + +--- + +## Implementing the action handler + +After scaffolding, you'll have a stub handler at `ts/packages/agents/slack/src/slackActionHandler.ts`. The stub looks like: + +```typescript +async function executeAction( + action: TypeAgentAction, + context: ActionContext, +): Promise { + // TODO: implement action handlers + return createActionResultFromTextDisplay( + `Executing ${action.actionName} — not yet implemented.`, + ); +} +``` + +Fill in the cases using the Slack Web API client. For example: + +```typescript +switch (action.actionName) { + case "postMessage": { + const result = await slackClient.chat.postMessage({ + channel: action.parameters.channelId, + text: action.parameters.message, + }); + return createActionResultFromTextDisplay( + `Message sent to ${action.parameters.channelId}`, + ); + } + // ... +} +``` + +After implementing, rebuild: + +```bash +cd ts/packages/agents/slack +pnpm run build +``` + +Then re-run tests: + +``` +> Run tests for slack +``` + +--- + +## Sample session: rapid onboarding via AI assistant + +Here's a compressed example of what a full session looks like from the AI assistant's perspective: + +``` +You: I want to add a Jira integration to TypeAgent. It should support creating + issues, searching issues, updating issue status, and adding comments. + +AI: I'll kick off the Jira onboarding. Let me start by discovering the API surface. + + [calls startOnboarding for jira] + [calls parseOpenApiSpec from https://developer.atlassian.com/cloud/jira/platform/swagger-v3.v3.json] + Found 147 API actions. Here are a few: + - createIssue: Creates a new issue + - searchIssues: Searches for issues using JQL + - transitionIssue: Transitions an issue to a new status + - addComment: Adds a comment to an issue + ... + + I'll approve just the 4 you specified. + [calls approveApiSurface with includeActions: [createIssue, searchIssues, transitionIssue, addComment]] + [calls generatePhrases] + [calls approvePhrases] + [calls generateSchema] + + Here's the generated schema — looks good, I'll approve it. + [calls approveSchema] + [calls generateGrammar] + [calls compileGrammar] ✓ Compiled successfully + [calls approveGrammar] + [calls scaffoldAgent] + + The Jira agent is scaffolded at ts/packages/agents/jira/. Now I'll package + and register it so we can run tests. + + [calls packageAgent with register: true] + + TypeAgent needs a restart to pick up the new agent. Once you've restarted: + + [calls generateTests] + [calls runTests] + + Pass rate: 87% (35/40). Proposing repair for the 5 failing tests... + [calls proposeRepair] + [calls approveRepair] + [calls runTests] + + Pass rate: 97% (39/40). One edge case remaining in searchIssues — acceptable. + Onboarding complete! Implement slackActionHandler.ts to wire up the Jira API calls. +``` + +--- + +## Tips + +**Start narrow.** Onboard 4–6 actions first, get them working end-to-end, then add more. You can re-run `generatePhrases`, `generateSchema`, and `generateGrammar` incrementally. + +**Inspect artifacts directly.** All generated files are in `~/.typeagent/onboarding//`. You can edit them by hand before approving if the LLM output isn't quite right. + +**Grammar failures are normal.** The `.agr` compiler is strict. If `compileGrammar` fails, ask the AI to regenerate the grammar, or read the error and fix the specific rule. Common issues are ambiguous wildcards and missing required words before captures. + +**Test failures drive improvement.** A 70% pass rate on first run is typical. Two rounds of `proposeRepair` → `runTests` usually gets to 90%+. The LLM is good at diagnosing pattern mismatches. + +**Re-use grows over time.** The second integration you onboard will reuse the doc crawler, phrase generator, and schema generator — only the integration-specific configuration changes. diff --git a/ts/packages/agents/onboarding/package.json b/ts/packages/agents/onboarding/package.json new file mode 100644 index 0000000000..d7a09a6d90 --- /dev/null +++ b/ts/packages/agents/onboarding/package.json @@ -0,0 +1,61 @@ +{ + "name": "onboarding-agent", + "version": "0.0.1", + "private": true, + "description": "TypeAgent onboarding agent — automates integrating new applications into TypeAgent", + "homepage": "https://github.com/microsoft/TypeAgent#readme", + "repository": { + "type": "git", + "url": "https://github.com/microsoft/TypeAgent.git", + "directory": "ts/packages/agents/onboarding" + }, + "license": "MIT", + "author": "Microsoft", + "type": "module", + "exports": { + "./agent/manifest": "./src/onboardingManifest.json", + "./agent/handlers": "./dist/onboardingActionHandler.js" + }, + "scripts": { + "agc:discovery": "agc -i ./src/discovery/discoverySchema.agr -o ./dist/discoverySchema.ag.json", + "agc:grammargen": "agc -i ./src/grammarGen/grammarGenSchema.agr -o ./dist/grammarGenSchema.ag.json", + "agc:main": "agc -i ./src/onboardingSchema.agr -o ./dist/onboardingSchema.ag.json", + "agc:packaging": "agc -i ./src/packaging/packagingSchema.agr -o ./dist/packagingSchema.ag.json", + "agc:phrasegen": "agc -i ./src/phraseGen/phraseGenSchema.agr -o ./dist/phraseGenSchema.ag.json", + "agc:scaffolder": "agc -i ./src/scaffolder/scaffolderSchema.agr -o ./dist/scaffolderSchema.ag.json", + "agc:schemagen": "agc -i ./src/schemaGen/schemaGenSchema.agr -o ./dist/schemaGenSchema.ag.json", + "agc:testing": "agc -i ./src/testing/testingSchema.agr -o ./dist/testingSchema.ag.json", + "asc:discovery": "asc -i ./src/discovery/discoverySchema.ts -o ./dist/discoverySchema.pas.json -t DiscoveryActions", + "asc:grammargen": "asc -i ./src/grammarGen/grammarGenSchema.ts -o ./dist/grammarGenSchema.pas.json -t GrammarGenActions", + "asc:main": "asc -i ./src/onboardingSchema.ts -o ./dist/onboardingSchema.pas.json -t OnboardingActions", + "asc:packaging": "asc -i ./src/packaging/packagingSchema.ts -o ./dist/packagingSchema.pas.json -t PackagingActions", + "asc:phrasegen": "asc -i ./src/phraseGen/phraseGenSchema.ts -o ./dist/phraseGenSchema.pas.json -t PhraseGenActions", + "asc:scaffolder": "asc -i ./src/scaffolder/scaffolderSchema.ts -o ./dist/scaffolderSchema.pas.json -t ScaffolderActions", + "asc:schemagen": "asc -i ./src/schemaGen/schemaGenSchema.ts -o ./dist/schemaGenSchema.pas.json -t SchemaGenActions", + "asc:testing": "asc -i ./src/testing/testingSchema.ts -o ./dist/testingSchema.pas.json -t TestingActions", + "build": "concurrently npm:tsc npm:asc:* npm:agc:*", + "clean": "rimraf --glob dist *.tsbuildinfo *.done.build.log", + "prettier": "prettier --check . --ignore-path ../../../.prettierignore", + "prettier:fix": "prettier --write . --ignore-path ../../../.prettierignore", + "tsc": "tsc -b" + }, + "dependencies": { + "@typeagent/agent-sdk": "workspace:*", + "@typeagent/dispatcher-types": "workspace:*", + "agent-dispatcher": "workspace:*", + "aiclient": "workspace:*", + "dispatcher-node-providers": "workspace:*", + "typechat": "^0.1.1" + }, + "devDependencies": { + "@typeagent/action-schema-compiler": "workspace:*", + "action-grammar-compiler": "workspace:*", + "concurrently": "^9.1.2", + "prettier": "^3.5.3", + "rimraf": "^6.0.1", + "typescript": "~5.4.5" + }, + "engines": { + "node": ">=20" + } +} diff --git a/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts b/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts new file mode 100644 index 0000000000..79faa7ccf7 --- /dev/null +++ b/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts @@ -0,0 +1,641 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Phase 1 — Discovery handler. +// Enumerates the API surface of the target application from documentation +// or an OpenAPI spec, saving results to the workspace for the next phase. + +import { + ActionContext, + TypeAgentAction, + ActionResult, +} from "@typeagent/agent-sdk"; +import { createActionResultFromMarkdownDisplay } from "@typeagent/agent-sdk/helpers/action"; +import { DiscoveryActions } from "./discoverySchema.js"; +import { + loadState, + updatePhase, + writeArtifactJson, + readArtifactJson, +} from "../lib/workspace.js"; +import { getDiscoveryModel } from "../lib/llm.js"; + +// Represents a single discovered API action +export type DiscoveredAction = { + name: string; + description: string; + // HTTP method if REST, or operation type + method?: string; + // Endpoint path or function signature + path?: string; + // Discovered parameters + parameters?: DiscoveredParameter[]; + // Source URL where this was found + sourceUrl?: string; +}; + +export type DiscoveredParameter = { + name: string; + type: string; + description?: string; + required?: boolean; +}; + +export type ApiSurface = { + integrationName: string; + discoveredAt: string; + source: string; + actions: DiscoveredAction[]; + approved?: boolean; + approvedAt?: string; + approvedActions?: string[]; +}; + +export async function executeDiscoveryAction( + action: TypeAgentAction, + _context: ActionContext, +): Promise { + switch (action.actionName) { + case "crawlDocUrl": + return handleCrawlDocUrl( + action.parameters.integrationName, + action.parameters.url, + action.parameters.maxDepth ?? 2, + ); + + case "parseOpenApiSpec": + return handleParseOpenApiSpec( + action.parameters.integrationName, + action.parameters.specSource, + ); + + case "listDiscoveredActions": + return handleListDiscoveredActions( + action.parameters.integrationName, + ); + + case "approveApiSurface": + return handleApproveApiSurface( + action.parameters.integrationName, + action.parameters.includeActions, + action.parameters.excludeActions, + ); + } +} + +async function handleCrawlDocUrl( + integrationName: string, + url: string, + maxDepth: number, +): Promise { + const state = await loadState(integrationName); + if (!state) { + return { + error: `Integration "${integrationName}" not found. Run startOnboarding first.`, + }; + } + + await updatePhase(integrationName, "discovery", { status: "in-progress" }); + + const model = getDiscoveryModel(); + + // Fetch and parse the documentation page + let pageContent: string; + try { + const response = await fetch(url); + if (!response.ok) { + return { + error: `Failed to fetch ${url}: ${response.status} ${response.statusText}`, + }; + } + pageContent = await response.text(); + } catch (err: any) { + return { error: `Failed to fetch ${url}: ${err?.message ?? err}` }; + } + + // Strip HTML tags and collapse whitespace to get readable text content + const textContent = stripHtml(pageContent); + + // Follow links up to maxDepth levels + const linkedContent = await crawlLinks( + url, + pageContent, + maxDepth, + integrationName, + ); + + // Use LLM to extract API actions from the page content + const prompt = [ + { + role: "system" as const, + content: + "You are an API documentation analyzer. Extract a list of user-facing API actions/operations from the provided documentation. " + + "For each action, identify: name (camelCase), description, HTTP method (if applicable), endpoint path (if applicable), and parameters. " + + "IMPORTANT: Only include actions that represent real operations a user would invoke. " + + "Exclude internal/infrastructure methods like: load, sync, toJSON, context, track, untrack, set, get (bare getters/setters without a domain concept). " + + "Return a JSON array of actions with shape: { name, description, method?, path?, parameters?: [{name, type, description?, required?}] }[]", + }, + { + role: "user" as const, + content: + `Extract all user-facing API actions from this documentation for the "${integrationName}" integration.\n\n` + + `Primary URL: ${url}\n\n` + + `Content:\n${(textContent + "\n\n" + linkedContent).slice(0, 16000)}`, + }, + ]; + + const result = await model.complete(prompt); + if (!result.success) { + return { error: `LLM extraction failed: ${result.message}` }; + } + + let actions: DiscoveredAction[] = []; + try { + // Extract JSON from LLM response + const jsonMatch = result.data.match(/\[[\s\S]*\]/); + if (jsonMatch) { + actions = JSON.parse(jsonMatch[0]); + } + } catch { + return { error: "Failed to parse LLM response as JSON action list." }; + } + + // Add source URL to each action; filter out internal framework methods + actions = actions + .map((a) => ({ ...a, sourceUrl: url })) + .filter((a) => !isInternalAction(a.name)); + + // Merge with any existing discovered actions + const existing = await readArtifactJson( + integrationName, + "discovery", + "api-surface.json", + ); + const merged: ApiSurface = { + integrationName, + discoveredAt: new Date().toISOString(), + source: url, + actions: [ + ...(existing?.actions ?? []).filter( + (a) => !actions.find((n) => n.name === a.name), + ), + ...actions, + ], + }; + + await writeArtifactJson( + integrationName, + "discovery", + "api-surface.json", + merged, + ); + + return createActionResultFromMarkdownDisplay( + `## Discovery complete: ${integrationName}\n\n` + + `**Source:** ${url}\n` + + `**Actions found:** ${actions.length}\n\n` + + actions + .slice(0, 20) + .map((a) => `- **${a.name}**: ${a.description}`) + .join("\n") + + (actions.length > 20 + ? `\n\n_...and ${actions.length - 20} more_` + : "") + + `\n\nReview with \`listDiscoveredActions\`, then \`approveApiSurface\` to proceed.`, + ); +} + +// ── HTML helpers ───────────────────────────────────────────────────────────── + +// Strip HTML tags and collapse whitespace to extract readable text. +function stripHtml(html: string): string { + // Repeatedly remove multi-character patterns until stable to avoid + // incomplete sanitization from overlapping/re-formed substrings. + let sanitized = html; + let previous: string; + do { + previous = sanitized; + sanitized = sanitized + .replace(//gi, "") + .replace(//gi, ""); + } while (sanitized !== previous); + + return sanitized + .replace(/<[^>]+>/g, " ") + .replace(/ /g, " ") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, '"') + .replace(/&/g, "&") + .replace(/\s{2,}/g, " ") + .trim(); +} + +// Extract same-origin links from an HTML page. +function extractLinks(baseUrl: string, html: string): string[] { + const base = new URL(baseUrl); + const links: string[] = []; + const hrefRe = /href=["']([^"'#?]+)["']/gi; + let m: RegExpExecArray | null; + while ((m = hrefRe.exec(html)) !== null) { + try { + const resolved = new URL(m[1], baseUrl); + // Only follow links on the same hostname and path prefix + if ( + resolved.hostname === base.hostname && + resolved.pathname.startsWith( + base.pathname.split("/").slice(0, -1).join("/"), + ) + ) { + links.push(resolved.href); + } + } catch { + // skip malformed URLs + } + } + // Deduplicate + return [...new Set(links)].slice(0, 30); // cap at 30 links +} + +// Crawl linked pages up to maxDepth and return combined text (capped to 8000 chars per page). +async function crawlLinks( + baseUrl: string, + baseHtml: string, + maxDepth: number, + _integrationName: string, +): Promise { + if (maxDepth <= 1) return ""; + + const links = extractLinks(baseUrl, baseHtml); + const visited = new Set([baseUrl]); + const chunks: string[] = []; + + for (const link of links.slice(0, 15)) { + if (visited.has(link)) continue; + visited.add(link); + try { + const resp = await fetch(link); + if (!resp.ok) continue; + const html = await resp.text(); + const text = stripHtml(html).slice(0, 8000); + chunks.push(`\n--- ${link} ---\n${text}`); + } catch { + // skip unreachable pages + } + } + + return chunks.join("\n").slice(0, 40000); +} + +// Names that are internal Office.js / API framework infrastructure, not user-facing operations. +const INTERNAL_ACTION_NAMES = new Set([ + "load", + "sync", + "toJSON", + "track", + "untrack", + "context", + "getItem", + "getCount", + "getItemOrNullObject", + "getFirstOrNullObject", + "getLastOrNullObject", + "getLast", + "getFirst", + "items", +]); + +function isInternalAction(name: string): boolean { + if (INTERNAL_ACTION_NAMES.has(name)) return true; + // Bare getters/setters with no domain concept (e.g. "get", "set", "load") + if (/^(get|set|load|read|fetch)$/.test(name)) return true; + return false; +} + +async function handleParseOpenApiSpec( + integrationName: string, + specSource: string, +): Promise { + const state = await loadState(integrationName); + if (!state) { + return { + error: `Integration "${integrationName}" not found. Run startOnboarding first.`, + }; + } + + await updatePhase(integrationName, "discovery", { status: "in-progress" }); + + // Fetch the spec (URL or file path) + let specContent: string; + try { + if ( + specSource.startsWith("http://") || + specSource.startsWith("https://") + ) { + const response = await fetch(specSource); + if (!response.ok) { + return { + error: `Failed to fetch spec: ${response.status} ${response.statusText}`, + }; + } + specContent = await response.text(); + } else { + const fs = await import("fs/promises"); + specContent = await fs.readFile(specSource, "utf-8"); + } + } catch (err: any) { + return { + error: `Failed to read spec from ${specSource}: ${err?.message ?? err}`, + }; + } + + let spec: any; + try { + spec = JSON.parse(specContent); + } catch { + try { + // Try YAML if JSON fails (basic line parsing) + return { + error: "YAML specs not yet supported — please provide a JSON OpenAPI spec.", + }; + } catch { + return { error: "Could not parse spec as JSON or YAML." }; + } + } + + // Extract actions from OpenAPI paths + const actions: DiscoveredAction[] = []; + const paths = spec.paths ?? {}; + for (const [pathStr, pathItem] of Object.entries(paths) as [ + string, + any, + ][]) { + for (const method of [ + "get", + "post", + "put", + "patch", + "delete", + ] as const) { + const op = pathItem?.[method]; + if (!op) continue; + + const name = + op.operationId ?? + `${method}${pathStr.replace(/[^a-zA-Z0-9]/g, "_")}`; + const camelName = name.replace( + /_([a-z])/g, + (_: string, c: string) => c.toUpperCase(), + ); + + const parameters: DiscoveredParameter[] = (op.parameters ?? []).map( + (p: any) => ({ + name: p.name, + type: p.schema?.type ?? "string", + description: p.description, + required: p.required ?? false, + }), + ); + + // Also include request body fields as parameters + const requestBody = + op.requestBody?.content?.["application/json"]?.schema; + if (requestBody?.properties) { + for (const [propName, propSchema] of Object.entries( + requestBody.properties, + ) as [string, any][]) { + parameters.push({ + name: propName, + type: propSchema.type ?? "string", + description: propSchema.description, + required: + requestBody.required?.includes(propName) ?? false, + }); + } + } + + actions.push({ + name: camelName, + description: + op.summary ?? + op.description ?? + `${method.toUpperCase()} ${pathStr}`, + method: method.toUpperCase(), + path: pathStr, + parameters, + sourceUrl: specSource, + }); + } + } + + const surface: ApiSurface = { + integrationName, + discoveredAt: new Date().toISOString(), + source: specSource, + actions, + }; + + await writeArtifactJson( + integrationName, + "discovery", + "api-surface.json", + surface, + ); + + return createActionResultFromMarkdownDisplay( + `## OpenAPI spec parsed: ${integrationName}\n\n` + + `**Source:** ${specSource}\n` + + `**OpenAPI version:** ${spec.openapi ?? spec.swagger ?? "unknown"}\n` + + `**Actions found:** ${actions.length}\n\n` + + actions + .slice(0, 20) + .map( + (a) => + `- **${a.name}** (\`${a.method} ${a.path}\`): ${a.description}`, + ) + .join("\n") + + (actions.length > 20 + ? `\n\n_...and ${actions.length - 20} more_` + : "") + + `\n\nReview with \`listDiscoveredActions\`, then \`approveApiSurface\` to proceed.`, + ); +} + +async function handleListDiscoveredActions( + integrationName: string, +): Promise { + const surface = await readArtifactJson( + integrationName, + "discovery", + "api-surface.json", + ); + if (!surface) { + return { + error: `No discovered actions found for "${integrationName}". Run crawlDocUrl or parseOpenApiSpec first.`, + }; + } + + const lines = [ + `## Discovered actions: ${integrationName}`, + ``, + `**Source:** ${surface.source}`, + `**Discovered:** ${surface.discoveredAt}`, + `**Total actions:** ${surface.actions.length}`, + `**Status:** ${surface.approved ? "✅ Approved" : "⏳ Pending approval"}`, + ``, + `| # | Name | Description |`, + `|---|---|---|`, + ...surface.actions.map( + (a, i) => `| ${i + 1} | \`${a.name}\` | ${a.description} |`, + ), + ]; + + return createActionResultFromMarkdownDisplay(lines.join("\n")); +} + +async function handleApproveApiSurface( + integrationName: string, + includeActions?: string[], + excludeActions?: string[], +): Promise { + const surface = await readArtifactJson( + integrationName, + "discovery", + "api-surface.json", + ); + if (!surface) { + return { + error: `No discovered actions found for "${integrationName}".`, + }; + } + + let approved = surface.actions; + if (includeActions && includeActions.length > 0) { + approved = approved.filter((a) => includeActions.includes(a.name)); + } + if (excludeActions && excludeActions.length > 0) { + approved = approved.filter((a) => !excludeActions.includes(a.name)); + } + + const updated: ApiSurface = { + ...surface, + approved: true, + approvedAt: new Date().toISOString(), + approvedActions: approved.map((a) => a.name), + actions: approved, + }; + + await writeArtifactJson( + integrationName, + "discovery", + "api-surface.json", + updated, + ); + await updatePhase(integrationName, "discovery", { status: "approved" }); + + // If many actions, recommend sub-schema categorization + let subSchemaNote = ""; + if (approved.length > 20) { + subSchemaNote = await generateSubSchemaRecommendation( + integrationName, + approved, + ); + } + + return createActionResultFromMarkdownDisplay( + `## API surface approved: ${integrationName}\n\n` + + `**Approved actions:** ${approved.length}\n\n` + + approved + .map((a) => `- \`${a.name}\`: ${a.description}`) + .join("\n") + + subSchemaNote + + `\n\n**Next step:** Phase 2 — use \`generatePhrases\` to create natural language samples.`, + ); +} + +// When the approved action count exceeds 20, ask the LLM to categorize them +// into logical groups and save a sub-schema-groups.json artifact so that the +// scaffolder phase can generate sub-action manifests. +type SubSchemaGroup = { + name: string; + description: string; + actions: string[]; +}; + +type SubSchemaSuggestion = { + recommended: boolean; + groups: SubSchemaGroup[]; +}; + +async function generateSubSchemaRecommendation( + integrationName: string, + approved: DiscoveredAction[], +): Promise { + const model = getDiscoveryModel(); + const actionList = approved + .map((a) => `- ${a.name}: ${a.description}`) + .join("\n"); + + const prompt = [ + { + role: "system" as const, + content: + "You are an API architect. Given a list of API actions, categorize them " + + "into logical groups suitable for sub-schema separation in a TypeAgent agent. " + + "Each group should have a short camelCase name, a description, and the list of action names belonging to it. " + + "Every action must appear in exactly one group. Aim for 3-7 groups. " + + "Return ONLY a JSON array of objects with keys: name, description, actions.", + }, + { + role: "user" as const, + content: `Categorize these ${approved.length} actions for the "${integrationName}" integration into logical sub-schema groups:\n\n${actionList}`, + }, + ]; + + const result = await model.complete(prompt); + if (!result.success) { + // Non-fatal — just skip the recommendation + return "\n\n> **Note:** Could not generate sub-schema recommendation (LLM error). You can still proceed."; + } + + let groups: SubSchemaGroup[] = []; + try { + const jsonMatch = result.data.match(/\[[\s\S]*\]/); + if (jsonMatch) { + groups = JSON.parse(jsonMatch[0]); + } + } catch { + return "\n\n> **Note:** Could not parse sub-schema recommendation. You can still proceed."; + } + + if (groups.length === 0) { + return ""; + } + + const suggestion: SubSchemaSuggestion = { + recommended: true, + groups, + }; + + await writeArtifactJson( + integrationName, + "discovery", + "sub-schema-groups.json", + suggestion, + ); + + const groupSummary = groups + .map( + (g) => + `- **${g.name}** (${g.actions.length} actions): ${g.description}`, + ) + .join("\n"); + + return ( + `\n\n---\n### Sub-schema recommendation\n\n` + + `With **${approved.length} actions**, we recommend splitting into sub-schemas for better organization:\n\n` + + groupSummary + + `\n\nThis grouping has been saved to \`discovery/sub-schema-groups.json\`. ` + + `The scaffolder will use it to generate separate schema and grammar files per group.` + ); +} diff --git a/ts/packages/agents/onboarding/src/discovery/discoverySchema.agr b/ts/packages/agents/onboarding/src/discovery/discoverySchema.agr new file mode 100644 index 0000000000..f130d13d93 --- /dev/null +++ b/ts/packages/agents/onboarding/src/discovery/discoverySchema.agr @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Grammar for Phase 1 — Discovery actions. + +// crawlDocUrl - crawl API documentation at a URL + = crawl (docs | documentation | api) (at | from)? $(url:wildcard) for $(integrationName:wildcard) -> { + actionName: "crawlDocUrl", + parameters: { + url, + integrationName + } +} + | (fetch | scrape | read) (the)? $(integrationName:wildcard) (api)? (docs | documentation) (at | from)? $(url:wildcard) -> { + actionName: "crawlDocUrl", + parameters: { + integrationName, + url + } +}; + +// parseOpenApiSpec - parse an OpenAPI or Swagger spec + = parse (the)? (openapi | swagger | api) spec (at | from)? $(specSource:wildcard) for $(integrationName:wildcard) -> { + actionName: "parseOpenApiSpec", + parameters: { + specSource, + integrationName + } +} + | (load | ingest) (the)? $(integrationName:wildcard) (openapi | swagger | api) spec (from | at)? $(specSource:wildcard) -> { + actionName: "parseOpenApiSpec", + parameters: { + integrationName, + specSource + } +}; + +// listDiscoveredActions - show what was found + = list (discovered | found)? actions for $(integrationName:wildcard) -> { + actionName: "listDiscoveredActions", + parameters: { + integrationName + } +} + | (show | what are) (the)? (discovered | available)? actions (for | in)? $(integrationName:wildcard) -> { + actionName: "listDiscoveredActions", + parameters: { + integrationName + } +}; + +// approveApiSurface - lock in the discovered action set + = approve (the)? (api)? (surface | actions) for $(integrationName:wildcard) -> { + actionName: "approveApiSurface", + parameters: { + integrationName + } +} + | (confirm | finalize) (the)? $(integrationName:wildcard) (api)? surface -> { + actionName: "approveApiSurface", + parameters: { + integrationName + } +}; + +import { DiscoveryActions } from "./discoverySchema.ts"; + + : DiscoveryActions = + | + | + | ; diff --git a/ts/packages/agents/onboarding/src/discovery/discoverySchema.ts b/ts/packages/agents/onboarding/src/discovery/discoverySchema.ts new file mode 100644 index 0000000000..44d27c9bcc --- /dev/null +++ b/ts/packages/agents/onboarding/src/discovery/discoverySchema.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export type DiscoveryActions = + | CrawlDocUrlAction + | ParseOpenApiSpecAction + | ListDiscoveredActionsAction + | ApproveApiSurfaceAction; + +export type CrawlDocUrlAction = { + actionName: "crawlDocUrl"; + parameters: { + // Name of the integration being onboarded + integrationName: string; + // URL of the API documentation page to crawl (e.g. "https://api.slack.com/methods") + url: string; + // Maximum link-follow depth (default: 2) + maxDepth?: number; + }; +}; + +export type ParseOpenApiSpecAction = { + actionName: "parseOpenApiSpec"; + parameters: { + // Name of the integration being onboarded + integrationName: string; + // URL or absolute file path to the OpenAPI 3.x or Swagger 2.x spec + specSource: string; + }; +}; + +export type ListDiscoveredActionsAction = { + actionName: "listDiscoveredActions"; + parameters: { + // Integration name to list discovered actions for + integrationName: string; + }; +}; + +export type ApproveApiSurfaceAction = { + actionName: "approveApiSurface"; + parameters: { + // Integration name to approve + integrationName: string; + // If provided, only these action names are included in the approved surface (excludes all others) + includeActions?: string[]; + // Action names to exclude from the approved surface + excludeActions?: string[]; + }; +}; diff --git a/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts b/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts new file mode 100644 index 0000000000..898074830b --- /dev/null +++ b/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts @@ -0,0 +1,297 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Phase 4 — Grammar Generation handler. +// Generates a .agr grammar file from the approved schema and phrase set, +// then compiles it via the action-grammar-compiler (agc) to validate. + +import { + ActionContext, + TypeAgentAction, + ActionResult, +} from "@typeagent/agent-sdk"; +import { createActionResultFromMarkdownDisplay } from "@typeagent/agent-sdk/helpers/action"; +import { GrammarGenActions } from "./grammarGenSchema.js"; +import { + loadState, + updatePhase, + writeArtifact, + readArtifact, + readArtifactJson, + getPhasePath, +} from "../lib/workspace.js"; +import { getGrammarGenModel } from "../lib/llm.js"; +import { ApiSurface } from "../discovery/discoveryHandler.js"; +import { PhraseSet } from "../phraseGen/phraseGenHandler.js"; +import { spawn } from "child_process"; +import fs from "fs/promises"; +import path from "path"; +import { fileURLToPath } from "url"; + +export async function executeGrammarGenAction( + action: TypeAgentAction, + _context: ActionContext, +): Promise { + switch (action.actionName) { + case "generateGrammar": + return handleGenerateGrammar(action.parameters.integrationName); + case "compileGrammar": + return handleCompileGrammar(action.parameters.integrationName); + case "approveGrammar": + return handleApproveGrammar(action.parameters.integrationName); + } +} + +async function handleGenerateGrammar( + integrationName: string, +): Promise { + const state = await loadState(integrationName); + if (!state) return { error: `Integration "${integrationName}" not found.` }; + if (state.phases.schemaGen.status !== "approved") { + return { + error: `Schema phase must be approved first. Run approveSchema.`, + }; + } + + const surface = await readArtifactJson( + integrationName, + "discovery", + "api-surface.json", + ); + const phraseSet = await readArtifactJson( + integrationName, + "phraseGen", + "phrases.json", + ); + const schemaTs = await readArtifact( + integrationName, + "schemaGen", + "schema.ts", + ); + if (!surface || !phraseSet || !schemaTs) { + return { + error: `Missing required artifacts for "${integrationName}".`, + }; + } + + await updatePhase(integrationName, "grammarGen", { status: "in-progress" }); + + const model = getGrammarGenModel(); + const prompt = buildGrammarPrompt( + integrationName, + surface, + phraseSet, + schemaTs, + ); + const result = await model.complete(prompt); + if (!result.success) { + return { error: `Grammar generation failed: ${result.message}` }; + } + + const grammarContent = extractGrammarContent(result.data); + await writeArtifact( + integrationName, + "grammarGen", + "schema.agr", + grammarContent, + ); + + return createActionResultFromMarkdownDisplay( + `## Grammar generated: ${integrationName}\n\n` + + "```\n" + + grammarContent.slice(0, 2000) + + (grammarContent.length > 2000 ? "\n// ... (truncated)" : "") + + "\n```\n\n" + + `Use \`compileGrammar\` to validate, or \`approveGrammar\` if it looks correct.`, + ); +} + +async function handleCompileGrammar( + integrationName: string, +): Promise { + const grammarPath = path.join( + getPhasePath(integrationName, "grammarGen"), + "schema.agr", + ); + const outputPath = path.join( + getPhasePath(integrationName, "grammarGen"), + "schema.ag.json", + ); + + const grammarContent = await readArtifact( + integrationName, + "grammarGen", + "schema.agr", + ); + if (!grammarContent) { + return { + error: `No grammar file found for "${integrationName}". Run generateGrammar first.`, + }; + } + + // Copy the schema .ts file into grammarGen/ so the agr import resolves + const schemaSrc = path.join( + getPhasePath(integrationName, "schemaGen"), + "schema.ts", + ); + const schemaDst = path.join( + getPhasePath(integrationName, "grammarGen"), + "schema.ts", + ); + try { + await fs.copyFile(schemaSrc, schemaDst); + } catch { + return { + error: `Could not copy schema.ts into grammarGen/ for compilation. Ensure schema is approved.`, + }; + } + + return new Promise((resolve) => { + // Resolve agc from the package's own node_modules/.bin + const pkgDir = path.resolve( + fileURLToPath(import.meta.url), + "..", + "..", + "..", + ); + const binDir = path.join(pkgDir, "node_modules", ".bin"); + const env = { + ...process.env, + PATH: binDir + path.delimiter + (process.env.PATH ?? ""), + }; + + const proc = spawn("agc", ["-i", grammarPath, "-o", outputPath], { + stdio: ["ignore", "pipe", "pipe"], + env, + shell: true, + }); + + let stdout = ""; + let stderr = ""; + proc.stdout?.on("data", (d: Buffer) => { + stdout += d.toString(); + }); + proc.stderr?.on("data", (d: Buffer) => { + stderr += d.toString(); + }); + + proc.on("close", (code) => { + if (code === 0) { + resolve( + createActionResultFromMarkdownDisplay( + `## Grammar compiled successfully: ${integrationName}\n\n` + + `Output: \`schema.ag.json\`\n\n` + + (stdout + ? `Compiler output:\n\`\`\`\n${stdout}\n\`\`\`` + : "") + + `\n\nUse \`approveGrammar\` to proceed to scaffolding.`, + ), + ); + } else { + resolve({ + error: + `Grammar compilation failed (exit code ${code}).\n\n` + + (stderr || stdout || "No output from compiler.") + + `\n\nUse \`generateGrammar\` or \`refineSchema\` to fix the grammar.`, + }); + } + }); + + proc.on("error", (err) => { + resolve({ + error: `Failed to run agc: ${err.message}. Is action-grammar-compiler installed?`, + }); + }); + }); +} + +async function handleApproveGrammar( + integrationName: string, +): Promise { + const grammar = await readArtifact( + integrationName, + "grammarGen", + "schema.agr", + ); + if (!grammar) { + return { + error: `No grammar found for "${integrationName}". Run generateGrammar first.`, + }; + } + + await updatePhase(integrationName, "grammarGen", { status: "approved" }); + + return createActionResultFromMarkdownDisplay( + `## Grammar approved: ${integrationName}\n\n` + + `**Next step:** Phase 5 — use \`scaffoldAgent\` to create the agent package.`, + ); +} + +function buildGrammarPrompt( + integrationName: string, + surface: ApiSurface, + phraseSet: PhraseSet, + schemaTs: string, +): { role: "system" | "user"; content: string }[] { + const actionExamples = surface.actions + .map((a) => { + const phrases = phraseSet.phrases[a.name] ?? []; + return `Action: ${a.name}\nPhrases:\n${phrases + .slice(0, 4) + .map((p) => ` - "${p}"`) + .join("\n")}`; + }) + .join("\n\n"); + + return [ + { + role: "system", + content: + "You are an expert in TypeAgent grammar files (.agr format). " + + "Grammar rules use this syntax:\n" + + ' = pattern -> { actionName: "name", parameters: { ... } }\n' + + " | alternative -> { ... };\n\n" + + "Pattern syntax:\n" + + " - $(paramName:wildcard) captures 1+ words into a variable\n" + + " - $(paramName:word) captures exactly 1 word into a variable\n" + + " - (optional)? makes tokens optional\n" + + " - word matches a literal word\n" + + " - | separates alternatives\n\n" + + "IMPORTANT: In the action output object after ->, reference captured parameters by BARE NAME only, NOT with $() syntax.\n" + + "Example:\n" + + " = add $(item:wildcard) to (the)? $(listName:wildcard) list -> {\n" + + ' actionName: "addItems",\n' + + " parameters: {\n" + + " items: [item],\n" + + " listName\n" + + " }\n" + + " };\n\n" + + "The action output must use multi-line format with proper indentation as shown above.\n" + + "The file must start with a copyright header comment and end with:\n" + + ' import { ActionType } from "./schemaFile.ts";\n' + + " : ActionType = | | ...;\n\n" + + "Respond in JSON format. Return a JSON object with a single `grammar` key containing the .agr file content as a string.", + }, + { + role: "user", + content: + `Generate a TypeAgent .agr grammar file for the "${integrationName}" integration.\n\n` + + `TypeScript schema:\n\`\`\`typescript\n${schemaTs.slice(0, 3000)}\n\`\`\`\n\n` + + `Sample phrases for each action:\n${actionExamples}\n\n` + + `The schema file will be imported as "./schema.ts". The entry type is the main union type from the schema.`, + }, + ]; +} + +function extractGrammarContent(llmResponse: string): string { + // Try to parse as JSON first (when using json_object response format) + try { + const parsed = JSON.parse(llmResponse); + if (parsed.grammar) return parsed.grammar.trim(); + } catch { + // Not JSON, fall through to other extraction methods + } + const fenceMatch = llmResponse.match(/```(?:agr)?\n([\s\S]*?)```/); + if (fenceMatch) return fenceMatch[1].trim(); + return llmResponse.trim(); +} diff --git a/ts/packages/agents/onboarding/src/grammarGen/grammarGenSchema.agr b/ts/packages/agents/onboarding/src/grammarGen/grammarGenSchema.agr new file mode 100644 index 0000000000..49b3b55a76 --- /dev/null +++ b/ts/packages/agents/onboarding/src/grammarGen/grammarGenSchema.agr @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Grammar for Phase 4 — Grammar Generation actions. + + = generate (the)? (agr)? grammar for $(integrationName:wildcard) -> { + actionName: "generateGrammar", + parameters: { + integrationName + } +} + | (create | produce | write) (the)? $(integrationName:wildcard) (agr)? grammar (file)? -> { + actionName: "generateGrammar", + parameters: { + integrationName + } +}; + + = compile (the)? $(integrationName:wildcard) grammar -> { + actionName: "compileGrammar", + parameters: { + integrationName + } +} + | (validate | build | check) (the)? $(integrationName:wildcard) grammar -> { + actionName: "compileGrammar", + parameters: { + integrationName + } +}; + + = approve (the)? $(integrationName:wildcard) grammar -> { + actionName: "approveGrammar", + parameters: { + integrationName + } +} + | (confirm | finalize) (the)? $(integrationName:wildcard) (agr)? grammar -> { + actionName: "approveGrammar", + parameters: { + integrationName + } +}; + +import { GrammarGenActions } from "./grammarGenSchema.ts"; + + : GrammarGenActions = + | + | ; diff --git a/ts/packages/agents/onboarding/src/grammarGen/grammarGenSchema.ts b/ts/packages/agents/onboarding/src/grammarGen/grammarGenSchema.ts new file mode 100644 index 0000000000..4f5f258734 --- /dev/null +++ b/ts/packages/agents/onboarding/src/grammarGen/grammarGenSchema.ts @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export type GrammarGenActions = + | GenerateGrammarAction + | CompileGrammarAction + | ApproveGrammarAction; + +export type GenerateGrammarAction = { + actionName: "generateGrammar"; + parameters: { + // Integration name to generate grammar for + integrationName: string; + }; +}; + +export type CompileGrammarAction = { + actionName: "compileGrammar"; + parameters: { + // Integration name whose grammar to compile and validate + integrationName: string; + }; +}; + +export type ApproveGrammarAction = { + actionName: "approveGrammar"; + parameters: { + // Integration name to approve grammar for + integrationName: string; + }; +}; diff --git a/ts/packages/agents/onboarding/src/lib/llm.ts b/ts/packages/agents/onboarding/src/lib/llm.ts new file mode 100644 index 0000000000..e79e6b6d06 --- /dev/null +++ b/ts/packages/agents/onboarding/src/lib/llm.ts @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// aiclient ChatModel factories for each onboarding phase. +// Each phase gets a distinct debug tag so LLM calls are easy to trace +// with DEBUG=typeagent:openai:* environment variable. +// +// Credentials are read from ts/.env via the standard TypeAgent mechanism. + +import { ChatModel, openai } from "aiclient"; + +export function getDiscoveryModel(): ChatModel { + return openai.createChatModelDefault("onboarding:discovery"); +} + +export function getPhraseGenModel(): ChatModel { + return openai.createChatModelDefault("onboarding:phrasegen"); +} + +export function getSchemaGenModel(): ChatModel { + return openai.createChatModelDefault("onboarding:schemagen"); +} + +export function getGrammarGenModel(): ChatModel { + return openai.createChatModelDefault("onboarding:grammargen"); +} + +export function getTestingModel(): ChatModel { + return openai.createChatModelDefault("onboarding:testing"); +} + +export function getPackagingModel(): ChatModel { + return openai.createChatModelDefault("onboarding:packaging"); +} diff --git a/ts/packages/agents/onboarding/src/lib/workspace.ts b/ts/packages/agents/onboarding/src/lib/workspace.ts new file mode 100644 index 0000000000..1b76879e8c --- /dev/null +++ b/ts/packages/agents/onboarding/src/lib/workspace.ts @@ -0,0 +1,214 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Manages per-integration workspace state persisted to disk. +// Each integration gets a folder at ~/.typeagent/onboarding// +// containing state.json and phase-specific artifact subdirectories. + +import fs from "fs/promises"; +import path from "path"; +import os from "os"; + +export type PhaseStatus = "pending" | "in-progress" | "approved" | "skipped"; + +export type PhaseState = { + status: PhaseStatus; + startedAt?: string; + completedAt?: string; +}; + +export type OnboardingPhase = + | "discovery" + | "phraseGen" + | "schemaGen" + | "grammarGen" + | "scaffolder" + | "testing" + | "packaging"; + +export const PHASE_ORDER: OnboardingPhase[] = [ + "discovery", + "phraseGen", + "schemaGen", + "grammarGen", + "scaffolder", + "testing", + "packaging", +]; + +export type OnboardingConfig = { + integrationName: string; + description?: string; + apiType?: "rest" | "graphql" | "websocket" | "ipc" | "sdk"; + docSources?: string[]; +}; + +export type OnboardingState = { + integrationName: string; + createdAt: string; + updatedAt: string; + // "complete" when all phases are approved + currentPhase: OnboardingPhase | "complete"; + config: OnboardingConfig; + phases: Record; +}; + +const BASE_DIR = path.join(os.homedir(), ".typeagent", "onboarding"); + +export function getWorkspacePath(integrationName: string): string { + return path.join(BASE_DIR, integrationName); +} + +export function getPhasePath( + integrationName: string, + phase: OnboardingPhase, +): string { + return path.join(getWorkspacePath(integrationName), phase); +} + +export async function createWorkspace( + config: OnboardingConfig, +): Promise { + const workspacePath = getWorkspacePath(config.integrationName); + await fs.mkdir(workspacePath, { recursive: true }); + + const emptyPhase = (): PhaseState => ({ status: "pending" }); + + const state: OnboardingState = { + integrationName: config.integrationName, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + currentPhase: "discovery", + config, + phases: { + discovery: emptyPhase(), + phraseGen: emptyPhase(), + schemaGen: emptyPhase(), + grammarGen: emptyPhase(), + scaffolder: emptyPhase(), + testing: emptyPhase(), + packaging: emptyPhase(), + }, + }; + + // Create phase subdirectories up front + for (const phase of PHASE_ORDER) { + await fs.mkdir(path.join(workspacePath, phase), { recursive: true }); + } + + await saveState(state); + return state; +} + +export async function loadState( + integrationName: string, +): Promise { + const statePath = path.join( + getWorkspacePath(integrationName), + "state.json", + ); + try { + const content = await fs.readFile(statePath, "utf-8"); + return JSON.parse(content) as OnboardingState; + } catch { + return undefined; + } +} + +export async function saveState(state: OnboardingState): Promise { + state.updatedAt = new Date().toISOString(); + const statePath = path.join( + getWorkspacePath(state.integrationName), + "state.json", + ); + await fs.writeFile(statePath, JSON.stringify(state, null, 2), "utf-8"); +} + +export async function updatePhase( + integrationName: string, + phase: OnboardingPhase, + update: Partial, +): Promise { + const state = await loadState(integrationName); + if (!state) { + throw new Error(`Integration "${integrationName}" not found`); + } + state.phases[phase] = { ...state.phases[phase], ...update }; + + // When approved, advance currentPhase to the next phase + if (update.status === "approved") { + state.phases[phase].completedAt = new Date().toISOString(); + const idx = PHASE_ORDER.indexOf(phase); + if (idx >= 0 && idx < PHASE_ORDER.length - 1) { + state.currentPhase = PHASE_ORDER[idx + 1]; + } else if (idx === PHASE_ORDER.length - 1) { + state.currentPhase = "complete"; + } + } + + if (update.status === "in-progress" && !state.phases[phase].startedAt) { + state.phases[phase].startedAt = new Date().toISOString(); + } + + await saveState(state); + return state; +} + +export async function readArtifact( + integrationName: string, + phase: OnboardingPhase, + filename: string, +): Promise { + const filePath = path.join(getPhasePath(integrationName, phase), filename); + try { + return await fs.readFile(filePath, "utf-8"); + } catch { + return undefined; + } +} + +export async function writeArtifact( + integrationName: string, + phase: OnboardingPhase, + filename: string, + content: string, +): Promise { + const dirPath = getPhasePath(integrationName, phase); + await fs.mkdir(dirPath, { recursive: true }); + const filePath = path.join(dirPath, filename); + await fs.writeFile(filePath, content, "utf-8"); + return filePath; +} + +export async function readArtifactJson( + integrationName: string, + phase: OnboardingPhase, + filename: string, +): Promise { + const content = await readArtifact(integrationName, phase, filename); + if (!content) return undefined; + return JSON.parse(content) as T; +} + +export async function writeArtifactJson( + integrationName: string, + phase: OnboardingPhase, + filename: string, + data: unknown, +): Promise { + return writeArtifact( + integrationName, + phase, + filename, + JSON.stringify(data, null, 2), + ); +} + +export async function listIntegrations(): Promise { + try { + const entries = await fs.readdir(BASE_DIR, { withFileTypes: true }); + return entries.filter((e) => e.isDirectory()).map((e) => e.name); + } catch { + return []; + } +} diff --git a/ts/packages/agents/onboarding/src/onboardingActionHandler.ts b/ts/packages/agents/onboarding/src/onboardingActionHandler.ts new file mode 100644 index 0000000000..b329a56f00 --- /dev/null +++ b/ts/packages/agents/onboarding/src/onboardingActionHandler.ts @@ -0,0 +1,290 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { + ActionContext, + AppAgent, + TypeAgentAction, + ActionResult, +} from "@typeagent/agent-sdk"; +import { + createActionResultFromTextDisplay, + createActionResultFromMarkdownDisplay, +} from "@typeagent/agent-sdk/helpers/action"; +import { OnboardingActions } from "./onboardingSchema.js"; +import { DiscoveryActions } from "./discovery/discoverySchema.js"; +import { PhraseGenActions } from "./phraseGen/phraseGenSchema.js"; +import { SchemaGenActions } from "./schemaGen/schemaGenSchema.js"; +import { GrammarGenActions } from "./grammarGen/grammarGenSchema.js"; +import { ScaffolderActions } from "./scaffolder/scaffolderSchema.js"; +import { TestingActions } from "./testing/testingSchema.js"; +import { PackagingActions } from "./packaging/packagingSchema.js"; +import { executeDiscoveryAction } from "./discovery/discoveryHandler.js"; +import { executePhraseGenAction } from "./phraseGen/phraseGenHandler.js"; +import { executeSchemaGenAction } from "./schemaGen/schemaGenHandler.js"; +import { executeGrammarGenAction } from "./grammarGen/grammarGenHandler.js"; +import { executeScaffolderAction } from "./scaffolder/scaffolderHandler.js"; +import { executeTestingAction } from "./testing/testingHandler.js"; +import { executePackagingAction } from "./packaging/packagingHandler.js"; +import { + createWorkspace, + loadState, + listIntegrations, +} from "./lib/workspace.js"; + +type AllActions = + | OnboardingActions + | DiscoveryActions + | PhraseGenActions + | SchemaGenActions + | GrammarGenActions + | ScaffolderActions + | TestingActions + | PackagingActions; + +export function instantiate(): AppAgent { + return { + executeAction, + }; +} + +async function executeAction( + action: TypeAgentAction, + context: ActionContext, +): Promise { + const { actionName } = action as TypeAgentAction; + + // Top-level coordination actions + if ( + actionName === "startOnboarding" || + actionName === "resumeOnboarding" || + actionName === "getOnboardingStatus" || + actionName === "listIntegrations" + ) { + return executeOnboardingAction( + action as TypeAgentAction, + context, + ); + } + + // Discovery phase + if ( + actionName === "crawlDocUrl" || + actionName === "parseOpenApiSpec" || + actionName === "listDiscoveredActions" || + actionName === "approveApiSurface" + ) { + return executeDiscoveryAction( + action as TypeAgentAction, + context, + ); + } + + // Phrase generation phase + if ( + actionName === "generatePhrases" || + actionName === "addPhrase" || + actionName === "removePhrase" || + actionName === "approvePhrases" + ) { + return executePhraseGenAction( + action as TypeAgentAction, + context, + ); + } + + // Schema generation phase + if ( + actionName === "generateSchema" || + actionName === "refineSchema" || + actionName === "approveSchema" + ) { + return executeSchemaGenAction( + action as TypeAgentAction, + context, + ); + } + + // Grammar generation phase + if ( + actionName === "generateGrammar" || + actionName === "compileGrammar" || + actionName === "approveGrammar" + ) { + return executeGrammarGenAction( + action as TypeAgentAction, + context, + ); + } + + // Scaffolder phase + if ( + actionName === "scaffoldAgent" || + actionName === "scaffoldPlugin" || + actionName === "listTemplates" + ) { + return executeScaffolderAction( + action as TypeAgentAction, + context, + ); + } + + // Testing phase + if ( + actionName === "generateTests" || + actionName === "runTests" || + actionName === "getTestResults" || + actionName === "proposeRepair" || + actionName === "approveRepair" + ) { + return executeTestingAction( + action as TypeAgentAction, + context, + ); + } + + // Packaging phase + if ( + actionName === "packageAgent" || + actionName === "validatePackage" || + actionName === "generateDemo" || + actionName === "generateReadme" + ) { + return executePackagingAction( + action as TypeAgentAction, + context, + ); + } + + return { error: `Unknown action: ${actionName}` }; +} + +async function executeOnboardingAction( + action: TypeAgentAction, + _context: ActionContext, +): Promise { + switch (action.actionName) { + case "startOnboarding": { + const { integrationName, description, apiType } = action.parameters; + const existing = await loadState(integrationName); + if (existing) { + return createActionResultFromTextDisplay( + `Integration "${integrationName}" already exists (current phase: ${existing.currentPhase}). Use resumeOnboarding to continue.`, + ); + } + await createWorkspace({ + integrationName, + ...(description !== undefined ? { description } : undefined), + ...(apiType !== undefined ? { apiType } : undefined), + }); + return createActionResultFromMarkdownDisplay( + `## Onboarding started: ${integrationName}\n\n` + + `**Next step:** Phase 1 — Discovery\n\n` + + `Use \`crawlDocUrl\` or \`parseOpenApiSpec\` to enumerate the API surface.\n\n` + + `Workspace: \`~/.typeagent/onboarding/${integrationName}/\``, + ); + } + + case "resumeOnboarding": { + const { integrationName, fromPhase } = action.parameters; + const state = await loadState(integrationName); + if (!state) { + return { + error: `Integration "${integrationName}" not found. Use startOnboarding to create it.`, + }; + } + const phase = fromPhase ?? state.currentPhase; + return createActionResultFromMarkdownDisplay( + `## Resuming: ${integrationName}\n\n` + + `**Current phase:** ${phase}\n\n` + + `${phaseNextStepHint(phase)}`, + ); + } + + case "getOnboardingStatus": { + const { integrationName } = action.parameters; + const state = await loadState(integrationName); + if (!state) { + return { + error: `Integration "${integrationName}" not found.`, + }; + } + const lines = [ + `## ${integrationName} — Onboarding Status`, + ``, + `**Current phase:** ${state.currentPhase}`, + `**Started:** ${state.createdAt}`, + `**Updated:** ${state.updatedAt}`, + ``, + `| Phase | Status |`, + `|---|---|`, + ...Object.entries(state.phases).map( + ([phase, ps]) => + `| ${phase} | ${statusIcon(ps.status)} ${ps.status} |`, + ), + ]; + return createActionResultFromMarkdownDisplay(lines.join("\n")); + } + + case "listIntegrations": { + const { status } = action.parameters; + const names = await listIntegrations(); + if (names.length === 0) { + return createActionResultFromTextDisplay( + "No integrations found. Use startOnboarding to begin.", + ); + } + const lines = [`## Integrations`, ``]; + for (const name of names) { + const state = await loadState(name); + if (!state) continue; + if (status === "complete" && state.currentPhase !== "complete") + continue; + if ( + status === "in-progress" && + state.currentPhase === "complete" + ) + continue; + lines.push( + `- **${name}** — ${state.currentPhase} (updated ${state.updatedAt})`, + ); + } + return createActionResultFromMarkdownDisplay(lines.join("\n")); + } + } +} + +function phaseNextStepHint(phase: string): string { + const hints: Record = { + discovery: + "Use `crawlDocUrl` or `parseOpenApiSpec` to enumerate the API surface.", + phraseGen: + "Use `generatePhrases` to create natural language samples for each action.", + schemaGen: + "Use `generateSchema` to produce the TypeScript action schema.", + grammarGen: + "Use `generateGrammar` to produce the .agr grammar file, then `compileGrammar` to validate.", + scaffolder: + "Use `scaffoldAgent` to stamp out the agent package infrastructure.", + testing: + "Use `generateTests` then `runTests` to validate phrase-to-action mapping.", + packaging: "Use `packageAgent` to prepare the agent for distribution.", + complete: "Onboarding is complete.", + }; + return hints[phase] ?? ""; +} + +function statusIcon(status: string): string { + switch (status) { + case "pending": + return "⏳"; + case "in-progress": + return "🔄"; + case "approved": + return "✅"; + case "skipped": + return "⏭️"; + default: + return "❓"; + } +} diff --git a/ts/packages/agents/onboarding/src/onboardingManifest.json b/ts/packages/agents/onboarding/src/onboardingManifest.json new file mode 100644 index 0000000000..22a5d7381d --- /dev/null +++ b/ts/packages/agents/onboarding/src/onboardingManifest.json @@ -0,0 +1,77 @@ +{ + "emojiChar": "🛠️", + "description": "Agent for onboarding new applications and APIs into TypeAgent", + "defaultEnabled": true, + "schema": { + "description": "Top-level onboarding coordination: start, resume, and check status of integration onboarding workflows", + "originalSchemaFile": "./onboardingSchema.ts", + "schemaFile": "../dist/onboardingSchema.pas.json", + "grammarFile": "../dist/onboardingSchema.ag.json", + "schemaType": "OnboardingActions" + }, + "subActionManifests": { + "onboarding-discovery": { + "schema": { + "description": "Phase 1: Enumerate the API surface of the target application by crawling documentation or parsing an OpenAPI spec", + "originalSchemaFile": "./discovery/discoverySchema.ts", + "schemaFile": "../dist/discoverySchema.pas.json", + "grammarFile": "../dist/discoverySchema.ag.json", + "schemaType": "DiscoveryActions" + } + }, + "onboarding-phrasegen": { + "schema": { + "description": "Phase 2: Generate natural language sample phrases that users would say to invoke each discovered action", + "originalSchemaFile": "./phraseGen/phraseGenSchema.ts", + "schemaFile": "../dist/phraseGenSchema.pas.json", + "grammarFile": "../dist/phraseGenSchema.ag.json", + "schemaType": "PhraseGenActions" + } + }, + "onboarding-schemagen": { + "schema": { + "description": "Phase 3: Generate TypeScript action schema types with comments that map user requests to the target API surface", + "originalSchemaFile": "./schemaGen/schemaGenSchema.ts", + "schemaFile": "../dist/schemaGenSchema.pas.json", + "grammarFile": "../dist/schemaGenSchema.ag.json", + "schemaType": "SchemaGenActions" + } + }, + "onboarding-grammargen": { + "schema": { + "description": "Phase 4: Generate .agr grammar files from action schemas and sample phrases, then compile and validate them", + "originalSchemaFile": "./grammarGen/grammarGenSchema.ts", + "schemaFile": "../dist/grammarGenSchema.pas.json", + "grammarFile": "../dist/grammarGenSchema.ag.json", + "schemaType": "GrammarGenActions" + } + }, + "onboarding-scaffolder": { + "schema": { + "description": "Phase 5: Scaffold the complete TypeAgent agent package infrastructure including manifest, handler, package.json, and any required plugins", + "originalSchemaFile": "./scaffolder/scaffolderSchema.ts", + "schemaFile": "../dist/scaffolderSchema.pas.json", + "grammarFile": "../dist/scaffolderSchema.ag.json", + "schemaType": "ScaffolderActions" + } + }, + "onboarding-testing": { + "schema": { + "description": "Phase 6: Generate test cases from sample phrases and run a phrase-to-action validation loop, proposing schema and grammar repairs for failures", + "originalSchemaFile": "./testing/testingSchema.ts", + "schemaFile": "../dist/testingSchema.pas.json", + "grammarFile": "../dist/testingSchema.ag.json", + "schemaType": "TestingActions" + } + }, + "onboarding-packaging": { + "schema": { + "description": "Phase 7: Package the completed agent for distribution and register it with the TypeAgent dispatcher", + "originalSchemaFile": "./packaging/packagingSchema.ts", + "schemaFile": "../dist/packagingSchema.pas.json", + "grammarFile": "../dist/packagingSchema.ag.json", + "schemaType": "PackagingActions" + } + } + } +} diff --git a/ts/packages/agents/onboarding/src/onboardingSchema.agr b/ts/packages/agents/onboarding/src/onboardingSchema.agr new file mode 100644 index 0000000000..a18f480ecc --- /dev/null +++ b/ts/packages/agents/onboarding/src/onboardingSchema.agr @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Grammar for top-level onboarding coordination actions. + +// startOnboarding - begin a new integration onboarding workflow + = start onboarding (for)? $(integrationName:wildcard) -> { + actionName: "startOnboarding", + parameters: { + integrationName + } +} + | onboard $(integrationName:wildcard) (into TypeAgent)? -> { + actionName: "startOnboarding", + parameters: { + integrationName + } +} + | (begin | create) (a)? (new)? $(integrationName:wildcard) integration -> { + actionName: "startOnboarding", + parameters: { + integrationName + } +}; + +// resumeOnboarding - continue an in-progress onboarding + = resume onboarding (for)? $(integrationName:wildcard) -> { + actionName: "resumeOnboarding", + parameters: { + integrationName + } +} + | continue (the)? $(integrationName:wildcard) onboarding -> { + actionName: "resumeOnboarding", + parameters: { + integrationName + } +}; + +// getOnboardingStatus - check the current phase and status + = (what's | what is) (the)? status (of)? (the)? $(integrationName:wildcard) onboarding -> { + actionName: "getOnboardingStatus", + parameters: { + integrationName + } +} + | show (me)? (the)? $(integrationName:wildcard) onboarding status -> { + actionName: "getOnboardingStatus", + parameters: { + integrationName + } +} + | how far along is (the)? $(integrationName:wildcard) (onboarding)? -> { + actionName: "getOnboardingStatus", + parameters: { + integrationName + } +}; + +// listIntegrations - list all known integrations + = list (all)? (my)? integrations -> { + actionName: "listIntegrations", + parameters: {} +} + | show (me)? (all)? (my)? integrations -> { + actionName: "listIntegrations", + parameters: {} +} + | what integrations (do I have | are there)? -> { + actionName: "listIntegrations", + parameters: {} +}; + +import { OnboardingActions } from "./onboardingSchema.ts"; + + : OnboardingActions = + | + | + | ; diff --git a/ts/packages/agents/onboarding/src/onboardingSchema.ts b/ts/packages/agents/onboarding/src/onboardingSchema.ts new file mode 100644 index 0000000000..6980d3cfc2 --- /dev/null +++ b/ts/packages/agents/onboarding/src/onboardingSchema.ts @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export type OnboardingActions = + | StartOnboardingAction + | ResumeOnboardingAction + | GetOnboardingStatusAction + | ListIntegrationsAction; + +export type StartOnboardingAction = { + actionName: "startOnboarding"; + parameters: { + // Unique name for this integration (e.g. "slack", "jira", "my-rest-api"). + // Used as the workspace folder name — lowercase, no spaces. + integrationName: string; + // Human-readable description of what the integration does + description?: string; + // The type of API being integrated; helps select appropriate templates and bridge patterns + apiType?: "rest" | "graphql" | "websocket" | "ipc" | "sdk"; + }; +}; + +export type ResumeOnboardingAction = { + actionName: "resumeOnboarding"; + parameters: { + // Name of the integration to resume + integrationName: string; + // Optional: override which phase to start from (defaults to current phase in state.json) + fromPhase?: + | "discovery" + | "phraseGen" + | "schemaGen" + | "grammarGen" + | "scaffolder" + | "testing" + | "packaging"; + }; +}; + +export type GetOnboardingStatusAction = { + actionName: "getOnboardingStatus"; + parameters: { + // Integration name to check status for + integrationName: string; + }; +}; + +export type ListIntegrationsAction = { + actionName: "listIntegrations"; + parameters: { + // Filter by phase status; omit to list all + status?: "in-progress" | "complete"; + }; +}; diff --git a/ts/packages/agents/onboarding/src/packaging/packagingHandler.ts b/ts/packages/agents/onboarding/src/packaging/packagingHandler.ts new file mode 100644 index 0000000000..7f902eada2 --- /dev/null +++ b/ts/packages/agents/onboarding/src/packaging/packagingHandler.ts @@ -0,0 +1,602 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Phase 7 — Packaging handler. +// Builds the scaffolded agent package and optionally registers it +// with the local TypeAgent dispatcher configuration. + +import { + ActionContext, + TypeAgentAction, + ActionResult, +} from "@typeagent/agent-sdk"; +import { createActionResultFromMarkdownDisplay } from "@typeagent/agent-sdk/helpers/action"; +import { PackagingActions } from "./packagingSchema.js"; +import { + loadState, + updatePhase, + readArtifact, + readArtifactJson, + writeArtifact, +} from "../lib/workspace.js"; +import { getPackagingModel } from "../lib/llm.js"; +import { spawn } from "child_process"; +import path from "path"; +import fs from "fs/promises"; + +export async function executePackagingAction( + action: TypeAgentAction, + _context: ActionContext, +): Promise { + switch (action.actionName) { + case "packageAgent": + return handlePackageAgent( + action.parameters.integrationName, + action.parameters.register ?? false, + ); + case "validatePackage": + return handleValidatePackage(action.parameters.integrationName); + case "generateDemo": + return handleGenerateDemo( + action.parameters.integrationName, + action.parameters.durationMinutes, + ); + case "generateReadme": + return handleGenerateReadme(action.parameters.integrationName); + } +} + +async function handlePackageAgent( + integrationName: string, + register: boolean, +): Promise { + const state = await loadState(integrationName); + if (!state) return { error: `Integration "${integrationName}" not found.` }; + if (state.phases.testing.status !== "approved") { + return { error: `Testing phase must be approved before packaging.` }; + } + + // Find where the scaffolded agent lives + const scaffoldedTo = await readArtifact( + integrationName, + "scaffolder", + "scaffolded-to.txt", + ); + if (!scaffoldedTo) { + return { error: `No scaffolded agent found. Run scaffoldAgent first.` }; + } + + const agentDir = scaffoldedTo.trim(); + + await updatePhase(integrationName, "packaging", { status: "in-progress" }); + + // Run pnpm install + build in the agent directory + const installResult = await runCommand("pnpm", ["install"], agentDir); + if (!installResult.success) { + return { + error: `pnpm install failed:\n${installResult.output}`, + }; + } + + const buildResult = await runCommand("pnpm", ["run", "build"], agentDir); + if (!buildResult.success) { + return { + error: `Build failed:\n${buildResult.output}`, + }; + } + + const summary = [ + `## Package built: ${integrationName}`, + ``, + `**Agent directory:** \`${agentDir}\``, + `**Build output:** \`${path.join(agentDir, "dist")}\``, + ``, + buildResult.output + ? `\`\`\`\n${buildResult.output.slice(0, 500)}\n\`\`\`` + : "", + ]; + + if (register) { + const registerResult = await registerWithDispatcher( + integrationName, + agentDir, + ); + summary.push(``, registerResult); + } + + await updatePhase(integrationName, "packaging", { status: "approved" }); + + summary.push( + ``, + `**Onboarding complete!** 🎉`, + ``, + `The \`${integrationName}\` agent is ready for end-user testing.`, + register + ? `It has been registered with the local TypeAgent dispatcher.` + : `Run with \`register: true\` to register with the local dispatcher, or add it manually to \`ts/packages/defaultAgentProvider/data/config.json\`.`, + ); + + return createActionResultFromMarkdownDisplay(summary.join("\n")); +} + +async function handleValidatePackage( + integrationName: string, +): Promise { + const scaffoldedTo = await readArtifact( + integrationName, + "scaffolder", + "scaffolded-to.txt", + ); + if (!scaffoldedTo) { + return { error: `No scaffolded agent found. Run scaffoldAgent first.` }; + } + + const agentDir = scaffoldedTo.trim(); + const checks: { name: string; passed: boolean; detail?: string }[] = []; + + // Check required files exist + const requiredFiles = [ + "package.json", + "tsconfig.json", + "src/tsconfig.json", + ]; + for (const file of requiredFiles) { + const exists = await fileExists(path.join(agentDir, file)); + checks.push({ name: `File: ${file}`, passed: exists }); + } + + // Check package.json exports + try { + const pkgJson = JSON.parse( + await fs.readFile(path.join(agentDir, "package.json"), "utf-8"), + ); + const hasManifestExport = !!pkgJson.exports?.["./agent/manifest"]; + const hasHandlerExport = !!pkgJson.exports?.["./agent/handlers"]; + checks.push({ + name: "package.json: exports ./agent/manifest", + passed: hasManifestExport, + }); + checks.push({ + name: "package.json: exports ./agent/handlers", + passed: hasHandlerExport, + }); + } catch { + checks.push({ + name: "package.json: parse", + passed: false, + detail: "Could not read package.json", + }); + } + + // Check dist exists (agent has been built) + const distExists = await fileExists(path.join(agentDir, "dist")); + checks.push({ name: "dist/ directory exists (built)", passed: distExists }); + + const passed = checks.filter((c) => c.passed).length; + const failed = checks.filter((c) => !c.passed).length; + + const lines = [ + `## Package validation: ${integrationName}`, + ``, + `**Passed:** ${passed} / ${checks.length}`, + ``, + ...checks.map( + (c) => + `${c.passed ? "✅" : "❌"} ${c.name}${c.detail ? ` — ${c.detail}` : ""}`, + ), + ]; + + if (failed === 0) { + lines.push(``, `Package is valid and ready for distribution.`); + } else { + lines.push(``, `Fix the failing checks above before packaging.`); + } + + return createActionResultFromMarkdownDisplay(lines.join("\n")); +} + +async function handleGenerateDemo( + integrationName: string, + durationMinutes?: string, +): Promise { + const state = await loadState(integrationName); + if (!state) return { error: `Integration "${integrationName}" not found.` }; + if (state.phases.testing.status !== "approved") { + return { + error: `Testing phase must be approved before generating a demo.`, + }; + } + + // Load discovery artifacts + const apiSurface = await readArtifactJson<{ + actions: { name: string; description: string; category?: string }[]; + }>(integrationName, "discovery", "api-surface.json"); + if (!apiSurface) { + return { + error: `No approved API surface found. Complete discovery first.`, + }; + } + + const subSchemaGroups = await readArtifactJson>( + integrationName, + "discovery", + "sub-schema-groups.json", + ); + + // Load the generated schema + const schemaTs = await readArtifact( + integrationName, + "schemaGen", + "schema.ts", + ); + + const duration = durationMinutes ?? "3-5"; + const description = state.config.description ?? integrationName; + + // Build action listing — grouped by sub-schema if available + let actionListing: string; + if (subSchemaGroups) { + const groupLines: string[] = []; + for (const [group, actionNames] of Object.entries(subSchemaGroups)) { + groupLines.push(`### ${group}`); + for (const actionName of actionNames) { + const action = apiSurface.actions.find( + (a) => a.name === actionName, + ); + groupLines.push( + `- **${actionName}**: ${action?.description ?? "(no description)"}`, + ); + } + groupLines.push(""); + } + actionListing = groupLines.join("\n"); + } else { + actionListing = apiSurface.actions + .map((a) => `- **${a.name}**: ${a.description}`) + .join("\n"); + } + + const model = getPackagingModel(); + const prompt = buildDemoPrompt( + integrationName, + description, + actionListing, + schemaTs ?? "", + duration, + ); + + const result = await model.complete(prompt); + if (!result.success) { + return { error: `Demo generation failed: ${result.message}` }; + } + + // Parse the LLM response — expect two fenced blocks: + // ```demo ... ``` and ```narration ... ``` + const responseText = result.data; + const demoScript = + extractFencedBlock(responseText, "demo") ?? + extractFirstFencedBlock(responseText) ?? + responseText; + const narrationScript = + extractFencedBlock(responseText, "narration") ?? + extractSecondFencedBlock(responseText) ?? + ""; + + // Find the scaffolded agent directory + const scaffoldedTo = await readArtifact( + integrationName, + "scaffolder", + "scaffolded-to.txt", + ); + + // Write to the shell demo directory alongside other demo scripts + const shellDemoDir = path.resolve( + scaffoldedTo?.trim() ?? ".", + "../../shell/demo", + ); + await fs.mkdir(shellDemoDir, { recursive: true }); + + const demoFilename = `${integrationName}_agent.txt`; + const narrationFilename = `${integrationName}_agent_narration.md`; + + const demoPath = path.join(shellDemoDir, demoFilename); + const narrationPath = path.join(shellDemoDir, narrationFilename); + + await fs.writeFile(demoPath, demoScript, "utf-8"); + await fs.writeFile(narrationPath, narrationScript, "utf-8"); + + // Also save as artifacts in the onboarding workspace + await writeArtifact(integrationName, "packaging", demoFilename, demoScript); + await writeArtifact( + integrationName, + "packaging", + narrationFilename, + narrationScript, + ); + + const lines = [ + `## Demo scripts generated: ${integrationName}`, + ``, + `**Demo script:** \`${demoPath}\``, + `**Narration script:** \`${narrationPath}\``, + ``, + `**Target duration:** ${duration} minutes`, + ``, + `### Demo script preview`, + `\`\`\``, + demoScript.split("\n").slice(0, 20).join("\n"), + demoScript.split("\n").length > 20 ? "..." : "", + `\`\`\``, + ``, + `### Narration preview`, + narrationScript.split("\n").slice(0, 15).join("\n"), + narrationScript.split("\n").length > 15 ? "\n..." : "", + ]; + + return createActionResultFromMarkdownDisplay(lines.join("\n")); +} + +function buildDemoPrompt( + integrationName: string, + description: string, + actionListing: string, + schemaTs: string, + duration: string, +): string { + return `You are generating a demo script for a TypeAgent integration called "${integrationName}". + +**Integration description:** ${description} + +**Available actions (grouped by category if applicable):** +${actionListing} + +${schemaTs ? `**TypeScript action schema:**\n\`\`\`typescript\n${schemaTs}\n\`\`\`` : ""} + +Generate TWO outputs: + +## 1. Demo script (shell format) + +Create a demo script with 5-8 acts that showcase each action category. The demo should be ${duration} minutes long (approximately 50-80 natural language commands). + +Format rules: +- One natural language command per line (what a user would type, NOT @action syntax) +- Use \`# Section Title\` comments for section headers +- Use \`@pauseForInput\` between acts/sections +- Commands should be realistic, conversational requests a user would make +- Progress from simple to complex usage +- Show off different capabilities in each act +- Include some multi-step scenarios + +Wrap the entire demo script in a fenced code block with the label \`demo\`: +\`\`\`demo +# Act 1: Getting Started +... +\`\`\` + +## 2. Narration script (markdown) + +Create a matching narration script with timestamped sections that correspond to each act. Include: +- Approximate timestamp for each section (e.g., [0:00], [0:30]) +- Voice-over text explaining what is being demonstrated +- Key talking points for each act +- Transition text between acts + +Wrap the narration in a fenced code block with the label \`narration\`: +\`\`\`narration +# Demo Narration: ${integrationName} Agent +... +\`\`\``; +} + +function extractFencedBlock(text: string, label: string): string | undefined { + const regex = new RegExp("```" + label + "\\s*\\n([\\s\\S]*?)\\n```", "i"); + const match = text.match(regex); + return match?.[1]?.trim(); +} + +function extractFirstFencedBlock(text: string): string | undefined { + const match = text.match(/```[\w]*\s*\n([\s\S]*?)\n```/); + return match?.[1]?.trim(); +} + +function extractSecondFencedBlock(text: string): string | undefined { + const blocks = [...text.matchAll(/```[\w]*\s*\n([\s\S]*?)\n```/g)]; + if (blocks.length >= 2) { + return blocks[1][1]?.trim(); + } + return undefined; +} + +async function registerWithDispatcher( + integrationName: string, + agentDir: string, +): Promise { + // Add agent to defaultAgentProvider config.json + const configPath = path.resolve( + agentDir, + "../../../../defaultAgentProvider/data/config.json", + ); + + try { + const configRaw = await fs.readFile(configPath, "utf-8"); + const config = JSON.parse(configRaw); + + if (!config.agents) config.agents = {}; + if (config.agents[integrationName]) { + return `Agent "${integrationName}" is already registered in the dispatcher config.`; + } + + config.agents[integrationName] = { + name: `${integrationName}-agent`, + }; + + await fs.writeFile( + configPath, + JSON.stringify(config, null, 2), + "utf-8", + ); + return `✅ Registered "${integrationName}" in dispatcher config at \`${configPath}\`\n\nRestart TypeAgent to load the new agent.`; + } catch (err: any) { + return `⚠️ Could not auto-register — update dispatcher config manually.\n${err?.message ?? err}`; + } +} + +async function runCommand( + cmd: string, + args: string[], + cwd: string, +): Promise<{ success: boolean; output: string }> { + return new Promise((resolve) => { + const proc = spawn(cmd, args, { + cwd, + stdio: ["ignore", "pipe", "pipe"], + shell: process.platform === "win32", + }); + + let output = ""; + proc.stdout?.on("data", (d: Buffer) => { + output += d.toString(); + }); + proc.stderr?.on("data", (d: Buffer) => { + output += d.toString(); + }); + + proc.on("close", (code) => { + resolve({ success: code === 0, output }); + }); + + proc.on("error", (err) => { + resolve({ success: false, output: err.message }); + }); + }); +} + +async function handleGenerateReadme( + integrationName: string, +): Promise { + const state = await loadState(integrationName); + if (!state) return { error: `Integration "${integrationName}" not found.` }; + + // Read artifacts for context + const surface = await readArtifactJson<{ + actions: { name: string; description: string }[]; + }>(integrationName, "discovery", "api-surface.json"); + const subGroups = await readArtifactJson<{ + recommended: boolean; + groups: { name: string; description: string; actions: string[] }[]; + }>(integrationName, "discovery", "sub-schema-groups.json"); + const scaffoldedTo = await readArtifact( + integrationName, + "scaffolder", + "scaffolded-to.txt", + ); + + const description = + state.config.description ?? `Agent for ${integrationName}`; + const totalActions = surface?.actions.length ?? 0; + + // Build action listing for the LLM + let actionListing: string; + if (subGroups?.recommended && subGroups.groups.length > 0) { + actionListing = subGroups.groups + .map( + (g) => + `**${g.name}** (${g.actions.length} actions) — ${g.description}\n` + + g.actions.map((a) => ` - ${a}`).join("\n"), + ) + .join("\n\n"); + } else { + actionListing = + surface?.actions + .map((a) => `- **${a.name}** — ${a.description}`) + .join("\n") ?? "No actions discovered."; + } + + const model = getPackagingModel(); + const prompt = [ + { + role: "system" as const, + content: + "You are a technical writer generating a README.md for a TypeAgent agent package. " + + "Write clear, concise documentation in GitHub-flavored Markdown. " + + "Include: overview, architecture diagram (ASCII), action categories table, " + + "prerequisites, quick start, manual setup, project structure, " + + "API limitations (if any actions report limitations), and troubleshooting. " + + "Respond in JSON format with a single `readme` key containing the full Markdown content.", + }, + { + role: "user" as const, + content: + `Generate a README.md for the "${integrationName}" TypeAgent agent.\n\n` + + `Description: ${description}\n\n` + + `Total actions: ${totalActions}\n\n` + + `Actions:\n${actionListing}\n\n` + + `The agent uses a WebSocket bridge pattern where a Node.js bridge server ` + + `connects to an Office Add-in running inside the application. ` + + `The bridge port is 5680. The add-in dev server runs on port 3003.\n\n` + + `The agent was created using the TypeAgent onboarding pipeline.\n\n` + + (subGroups?.recommended + ? `The agent uses ${subGroups.groups.length} sub-schemas: ${subGroups.groups.map((g) => g.name).join(", ")}.\n\n` + : "") + + `Include a quick start section that references:\n` + + ` pnpm run build packages/agents/${integrationName}\n` + + ` npx office-addin-dev-certs install\n` + + ` pnpm run ${integrationName}:addin\n`, + }, + ]; + + const result = await model.complete(prompt); + if (!result.success) { + return { error: `README generation failed: ${result.message}` }; + } + + // Extract README content + let readmeContent: string; + try { + const parsed = JSON.parse(result.data); + readmeContent = parsed.readme ?? result.data; + } catch { + readmeContent = result.data; + } + + // Write to the agent directory + const agentDir = scaffoldedTo?.trim(); + if (agentDir) { + try { + await fs.writeFile( + path.join(agentDir, "README.md"), + readmeContent, + "utf-8", + ); + } catch { + // Fall through — still save as artifact + } + } + + // Save as artifact + await writeArtifact( + integrationName, + "packaging", + "README.md", + readmeContent, + ); + + return createActionResultFromMarkdownDisplay( + `## README generated: ${integrationName}\n\n` + + (agentDir + ? `Written to \`${path.join(agentDir, "README.md")}\`\n\n` + : "") + + `**Preview (first 2000 chars):**\n\n` + + readmeContent.slice(0, 2000) + + (readmeContent.length > 2000 ? "\n\n_...truncated_" : ""), + ); +} + +async function fileExists(p: string): Promise { + try { + await fs.access(p); + return true; + } catch { + return false; + } +} diff --git a/ts/packages/agents/onboarding/src/packaging/packagingSchema.agr b/ts/packages/agents/onboarding/src/packaging/packagingSchema.agr new file mode 100644 index 0000000000..a306643cc6 --- /dev/null +++ b/ts/packages/agents/onboarding/src/packaging/packagingSchema.agr @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Grammar for Phase 7 — Packaging actions. + + = package (the)? $(integrationName:wildcard) agent -> { + actionName: "packageAgent", + parameters: { + integrationName + } +} + | (build | bundle | prepare) (the)? $(integrationName:wildcard) (agent)? (package)? (for distribution)? -> { + actionName: "packageAgent", + parameters: { + integrationName + } +}; + + = validate (the)? $(integrationName:wildcard) (agent)? package -> { + actionName: "validatePackage", + parameters: { + integrationName + } +} + | (check | verify) (the)? $(integrationName:wildcard) (agent)? package -> { + actionName: "validatePackage", + parameters: { + integrationName + } +}; + + = generate (a)? demo (script)? for $(integrationName:wildcard) -> { + actionName: "generateDemo", + parameters: { + integrationName + } +} + | create (a)? demo for $(integrationName:wildcard) -> { + actionName: "generateDemo", + parameters: { + integrationName + } +}; + + = generate (a)? readme for $(integrationName:wildcard) -> { + actionName: "generateReadme", + parameters: { + integrationName + } +} + | create (a)? readme for (the)? $(integrationName:wildcard) (agent)? -> { + actionName: "generateReadme", + parameters: { + integrationName + } +}; + +import { PackagingActions } from "./packagingSchema.ts"; + + : PackagingActions = + | + | + | ; diff --git a/ts/packages/agents/onboarding/src/packaging/packagingSchema.ts b/ts/packages/agents/onboarding/src/packaging/packagingSchema.ts new file mode 100644 index 0000000000..de3a68ae04 --- /dev/null +++ b/ts/packages/agents/onboarding/src/packaging/packagingSchema.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export type PackagingActions = + | PackageAgentAction + | ValidatePackageAction + | GenerateDemoAction + | GenerateReadmeAction; + +export type PackageAgentAction = { + actionName: "packageAgent"; + parameters: { + // Integration name to package + integrationName: string; + // If true, also register the agent with the local TypeAgent dispatcher config + register?: boolean; + }; +}; + +export type ValidatePackageAction = { + actionName: "validatePackage"; + parameters: { + // Integration name whose package to validate + integrationName: string; + }; +}; + +// Generates a README.md for the onboarded agent +export type GenerateReadmeAction = { + actionName: "generateReadme"; + parameters: { + // Name of the integration + integrationName: string; + }; +}; + +// Generates a demo script and narration for the onboarded agent +export type GenerateDemoAction = { + actionName: "generateDemo"; + parameters: { + // Name of the integration + integrationName: string; + // Duration target in minutes (default: 3-5) + durationMinutes?: string; + }; +}; diff --git a/ts/packages/agents/onboarding/src/phraseGen/phraseGenHandler.ts b/ts/packages/agents/onboarding/src/phraseGen/phraseGenHandler.ts new file mode 100644 index 0000000000..a98a934217 --- /dev/null +++ b/ts/packages/agents/onboarding/src/phraseGen/phraseGenHandler.ts @@ -0,0 +1,299 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Phase 2 — Phrase Generation handler. +// Generates natural language sample phrases for each discovered action +// using an LLM, saved to ~/.typeagent/onboarding//phraseGen/phrases.json + +import { + ActionContext, + TypeAgentAction, + ActionResult, +} from "@typeagent/agent-sdk"; +import { + createActionResultFromTextDisplay, + createActionResultFromMarkdownDisplay, +} from "@typeagent/agent-sdk/helpers/action"; +import { PhraseGenActions } from "./phraseGenSchema.js"; +import { + loadState, + updatePhase, + writeArtifactJson, + readArtifactJson, +} from "../lib/workspace.js"; +import { getPhraseGenModel } from "../lib/llm.js"; +import { ApiSurface, DiscoveredAction } from "../discovery/discoveryHandler.js"; + +export type PhraseSet = { + integrationName: string; + generatedAt: string; + // Map from actionName to array of sample phrases + phrases: Record; + approved?: boolean; + approvedAt?: string; +}; + +export async function executePhraseGenAction( + action: TypeAgentAction, + _context: ActionContext, +): Promise { + switch (action.actionName) { + case "generatePhrases": + return handleGeneratePhrases( + action.parameters.integrationName, + action.parameters.phrasesPerAction ?? 5, + action.parameters.forActions, + ); + + case "addPhrase": + return handleAddPhrase( + action.parameters.integrationName, + action.parameters.actionName, + action.parameters.phrase, + ); + + case "removePhrase": + return handleRemovePhrase( + action.parameters.integrationName, + action.parameters.actionName, + action.parameters.phrase, + ); + + case "approvePhrases": + return handleApprovePhrases(action.parameters.integrationName); + } +} + +async function handleGeneratePhrases( + integrationName: string, + phrasesPerAction: number, + forActions?: string[], +): Promise { + const state = await loadState(integrationName); + if (!state) { + return { error: `Integration "${integrationName}" not found.` }; + } + if (state.phases.discovery.status !== "approved") { + return { + error: `Discovery phase must be approved before generating phrases. Run approveApiSurface first.`, + }; + } + + const surface = await readArtifactJson( + integrationName, + "discovery", + "api-surface.json", + ); + if (!surface) { + return { error: `No API surface found for "${integrationName}".` }; + } + + await updatePhase(integrationName, "phraseGen", { status: "in-progress" }); + + const model = getPhraseGenModel(); + const existing = await readArtifactJson( + integrationName, + "phraseGen", + "phrases.json", + ); + const phraseMap: Record = existing?.phrases ?? {}; + + const actionsToProcess = forActions + ? surface.actions.filter((a) => forActions.includes(a.name)) + : surface.actions; + + for (const discoveredAction of actionsToProcess) { + const prompt = buildPhrasePrompt( + integrationName, + discoveredAction, + phrasesPerAction, + state.config.description, + ); + const result = await model.complete(prompt); + if (!result.success) continue; + + const phrases = extractPhraseList(result.data); + phraseMap[discoveredAction.name] = [ + ...(phraseMap[discoveredAction.name] ?? []), + ...phrases, + ]; + } + + const phraseSet: PhraseSet = { + integrationName, + generatedAt: new Date().toISOString(), + phrases: phraseMap, + }; + + await writeArtifactJson( + integrationName, + "phraseGen", + "phrases.json", + phraseSet, + ); + + const totalPhrases = Object.values(phraseMap).reduce( + (sum, p) => sum + p.length, + 0, + ); + + return createActionResultFromMarkdownDisplay( + `## Phrases generated: ${integrationName}\n\n` + + `**Actions covered:** ${Object.keys(phraseMap).length}\n` + + `**Total phrases:** ${totalPhrases}\n\n` + + Object.entries(phraseMap) + .slice(0, 10) + .map( + ([name, phrases]) => + `**${name}:**\n` + + phrases.map((p) => ` - "${p}"`).join("\n"), + ) + .join("\n\n") + + (Object.keys(phraseMap).length > 10 + ? `\n\n_...and ${Object.keys(phraseMap).length - 10} more actions_` + : "") + + `\n\nReview, add/remove phrases as needed, then \`approvePhrases\` to proceed.`, + ); +} + +async function handleAddPhrase( + integrationName: string, + actionName: string, + phrase: string, +): Promise { + const existing = await readArtifactJson( + integrationName, + "phraseGen", + "phrases.json", + ); + const phraseMap = existing?.phrases ?? {}; + if (!phraseMap[actionName]) phraseMap[actionName] = []; + if (!phraseMap[actionName].includes(phrase)) { + phraseMap[actionName].push(phrase); + } + + await writeArtifactJson(integrationName, "phraseGen", "phrases.json", { + ...(existing ?? { + integrationName, + generatedAt: new Date().toISOString(), + }), + phrases: phraseMap, + }); + + return createActionResultFromTextDisplay( + `Added phrase "${phrase}" to action "${actionName}" for ${integrationName}.`, + ); +} + +async function handleRemovePhrase( + integrationName: string, + actionName: string, + phrase: string, +): Promise { + const existing = await readArtifactJson( + integrationName, + "phraseGen", + "phrases.json", + ); + if (!existing) { + return { error: `No phrases found for "${integrationName}".` }; + } + + const phrases = existing.phrases[actionName] ?? []; + existing.phrases[actionName] = phrases.filter((p) => p !== phrase); + await writeArtifactJson( + integrationName, + "phraseGen", + "phrases.json", + existing, + ); + + return createActionResultFromTextDisplay( + `Removed phrase "${phrase}" from action "${actionName}" for ${integrationName}.`, + ); +} + +async function handleApprovePhrases( + integrationName: string, +): Promise { + const phraseSet = await readArtifactJson( + integrationName, + "phraseGen", + "phrases.json", + ); + if (!phraseSet) { + return { + error: `No phrases found for "${integrationName}". Run generatePhrases first.`, + }; + } + + const updated: PhraseSet = { + ...phraseSet, + approved: true, + approvedAt: new Date().toISOString(), + }; + + await writeArtifactJson( + integrationName, + "phraseGen", + "phrases.json", + updated, + ); + await updatePhase(integrationName, "phraseGen", { status: "approved" }); + + const totalPhrases = Object.values(phraseSet.phrases).reduce( + (sum, p) => sum + p.length, + 0, + ); + + return createActionResultFromMarkdownDisplay( + `## Phrases approved: ${integrationName}\n\n` + + `**Actions:** ${Object.keys(phraseSet.phrases).length}\n` + + `**Total phrases:** ${totalPhrases}\n\n` + + `**Next step:** Phase 3 — use \`generateSchema\` to produce the TypeScript action schema.`, + ); +} + +function buildPhrasePrompt( + integrationName: string, + action: DiscoveredAction, + count: number, + appDescription?: string, +): { role: "system" | "user"; content: string }[] { + return [ + { + role: "system", + content: + "You are a UX writer generating natural language phrases that users would say to an AI assistant to perform an API action. " + + "Produce varied, conversational phrases — include different phrasings, politeness levels, and levels of specificity. " + + "Return a JSON array of strings.", + }, + { + role: "user", + content: + `Generate ${count} distinct natural language phrases a user would say to perform this action in ${integrationName}` + + (appDescription ? ` (${appDescription})` : "") + + `.\n\n` + + `Action: ${action.name}\n` + + `Description: ${action.description}\n` + + (action.parameters?.length + ? `Parameters: ${action.parameters.map((p) => `${p.name} (${p.type})`).join(", ")}` + : "") + + `\n\nReturn only a JSON array of strings.`, + }, + ]; +} + +function extractPhraseList(llmResponse: string): string[] { + try { + const jsonMatch = llmResponse.match(/\[[\s\S]*\]/); + if (jsonMatch) { + const parsed = JSON.parse(jsonMatch[0]); + if (Array.isArray(parsed)) { + return parsed.filter((p) => typeof p === "string"); + } + } + } catch {} + // Fallback: extract quoted strings + return [...llmResponse.matchAll(/"([^"]+)"/g)].map((m) => m[1]); +} diff --git a/ts/packages/agents/onboarding/src/phraseGen/phraseGenSchema.agr b/ts/packages/agents/onboarding/src/phraseGen/phraseGenSchema.agr new file mode 100644 index 0000000000..ecfb22bde7 --- /dev/null +++ b/ts/packages/agents/onboarding/src/phraseGen/phraseGenSchema.agr @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Grammar for Phase 2 — Phrase Generation actions. + +// generatePhrases - generate natural language phrases for all or specific actions + = generate phrases for $(integrationName:wildcard) -> { + actionName: "generatePhrases", + parameters: { + integrationName + } +} + | (create | produce | write) (sample | example | natural language)? phrases for $(integrationName:wildcard) -> { + actionName: "generatePhrases", + parameters: { + integrationName + } +} + | generate $(phrasesPerAction:number) phrases (per action)? for $(integrationName:wildcard) -> { + actionName: "generatePhrases", + parameters: { + integrationName, + phrasesPerAction: phrasesPerAction + } +}; + +// addPhrase - manually add a phrase for a specific action + = add phrase $(phrase:wildcard) for (action)? $(actionName:wildcard) in $(integrationName:wildcard) -> { + actionName: "addPhrase", + parameters: { + phrase, + actionName, + integrationName + } +}; + +// removePhrase - remove a phrase from an action + = remove phrase $(phrase:wildcard) from (action)? $(actionName:wildcard) in $(integrationName:wildcard) -> { + actionName: "removePhrase", + parameters: { + phrase, + actionName, + integrationName + } +}; + +// approvePhrases - lock in the phrase set + = approve phrases for $(integrationName:wildcard) -> { + actionName: "approvePhrases", + parameters: { + integrationName + } +} + | (confirm | finalize) (the)? $(integrationName:wildcard) phrases -> { + actionName: "approvePhrases", + parameters: { + integrationName + } +}; + +import { PhraseGenActions } from "./phraseGenSchema.ts"; + + : PhraseGenActions = + | + | + | ; diff --git a/ts/packages/agents/onboarding/src/phraseGen/phraseGenSchema.ts b/ts/packages/agents/onboarding/src/phraseGen/phraseGenSchema.ts new file mode 100644 index 0000000000..f33b7def18 --- /dev/null +++ b/ts/packages/agents/onboarding/src/phraseGen/phraseGenSchema.ts @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export type PhraseGenActions = + | GeneratePhrasesAction + | AddPhraseAction + | RemovePhraseAction + | ApprovePhrasesAction; + +export type GeneratePhrasesAction = { + actionName: "generatePhrases"; + parameters: { + // Integration name to generate phrases for + integrationName: string; + // Number of phrases to generate per action (default: 5) + phrasesPerAction?: number; + // Generate phrases only for these specific action names (generates for all if omitted) + forActions?: string[]; + }; +}; + +export type AddPhraseAction = { + actionName: "addPhrase"; + parameters: { + // Integration name + integrationName: string; + // The action name this phrase should map to + actionName: string; + // The natural language phrase to add + phrase: string; + }; +}; + +export type RemovePhraseAction = { + actionName: "removePhrase"; + parameters: { + // Integration name + integrationName: string; + // The action name to remove the phrase from + actionName: string; + // The exact phrase to remove + phrase: string; + }; +}; + +export type ApprovePhrasesAction = { + actionName: "approvePhrases"; + parameters: { + // Integration name to approve phrases for + integrationName: string; + }; +}; diff --git a/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts b/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts new file mode 100644 index 0000000000..96bb718f72 --- /dev/null +++ b/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts @@ -0,0 +1,1372 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Phase 5 — Scaffolder handler. +// Stamps out a complete TypeAgent agent package from approved artifacts. +// Templates cover manifest, handler, schema, grammar, package.json, tsconfigs. + +import { + ActionContext, + TypeAgentAction, + ActionResult, +} from "@typeagent/agent-sdk"; +import { createActionResultFromMarkdownDisplay } from "@typeagent/agent-sdk/helpers/action"; +import { AgentPattern, ScaffolderActions } from "./scaffolderSchema.js"; +import { + loadState, + updatePhase, + writeArtifact, + readArtifact, + readArtifactJson, +} from "../lib/workspace.js"; +import fs from "fs/promises"; +import path from "path"; +import { fileURLToPath } from "url"; + +// Sub-schema group type matching discovery/sub-schema-groups.json +type SubSchemaGroup = { + name: string; + description: string; + actions: string[]; +}; + +type SubSchemaSuggestion = { + recommended: boolean; + groups: SubSchemaGroup[]; +}; + +// Default output root within the TypeAgent repo +const AGENTS_DIR = path.resolve( + fileURLToPath(import.meta.url), + "../../../../../../packages/agents", +); + +export async function executeScaffolderAction( + action: TypeAgentAction, + _context: ActionContext, +): Promise { + switch (action.actionName) { + case "scaffoldAgent": + return handleScaffoldAgent( + action.parameters.integrationName, + action.parameters.pattern, + action.parameters.outputDir, + ); + case "scaffoldPlugin": + return handleScaffoldPlugin( + action.parameters.integrationName, + action.parameters.template, + action.parameters.outputDir, + ); + case "listTemplates": + return handleListTemplates(); + case "listPatterns": + return handleListPatterns(); + } +} + +async function handleScaffoldAgent( + integrationName: string, + pattern: AgentPattern = "schema-grammar", + outputDir?: string, +): Promise { + const state = await loadState(integrationName); + if (!state) return { error: `Integration "${integrationName}" not found.` }; + if (state.phases.grammarGen.status !== "approved") { + return { + error: `Grammar phase must be approved first. Run approveGrammar.`, + }; + } + + const schemaTs = await readArtifact( + integrationName, + "schemaGen", + "schema.ts", + ); + const grammarAgr = await readArtifact( + integrationName, + "grammarGen", + "schema.agr", + ); + if (!schemaTs || !grammarAgr) { + return { + error: `Missing schema or grammar artifacts for "${integrationName}".`, + }; + } + + await updatePhase(integrationName, "scaffolder", { status: "in-progress" }); + + // Determine package name and Pascal-case type name + const packageName = `${integrationName}-agent`; + const pascalName = toPascalCase(integrationName); + const targetDir = outputDir ?? path.join(AGENTS_DIR, integrationName); + const srcDir = path.join(targetDir, "src"); + + await fs.mkdir(srcDir, { recursive: true }); + + // Check if sub-schema groups exist from the discovery phase + const subSchemaSuggestion = await readArtifactJson( + integrationName, + "discovery", + "sub-schema-groups.json", + ); + const subGroups = + subSchemaSuggestion?.recommended && + subSchemaSuggestion.groups.length > 0 + ? subSchemaSuggestion.groups + : undefined; + + // Write core schema and grammar + await writeFile(path.join(srcDir, `${integrationName}Schema.ts`), schemaTs); + await writeFile( + path.join(srcDir, `${integrationName}Schema.agr`), + grammarAgr.replace( + /from "\.\/schema\.ts"/g, + `from "./${integrationName}Schema.ts"`, + ), + ); + + // Track all files created for the output summary + const files: string[] = [ + `src/${integrationName}Schema.ts`, + `src/${integrationName}Schema.agr`, + ]; + + // If sub-schema groups exist, generate per-group schema and grammar files + if (subGroups) { + const actionsDir = path.join(srcDir, "actions"); + await fs.mkdir(actionsDir, { recursive: true }); + + for (const group of subGroups) { + const groupPascal = toPascalCase(group.name); + + // Generate a filtered schema file for this group + const groupSchemaContent = buildSubSchemaTs( + integrationName, + pascalName, + group, + groupPascal, + schemaTs, + ); + await writeFile( + path.join(actionsDir, `${group.name}ActionsSchema.ts`), + groupSchemaContent, + ); + files.push(`src/actions/${group.name}ActionsSchema.ts`); + + // Generate a filtered grammar file for this group + const groupGrammarContent = buildSubSchemaAgr( + integrationName, + group, + groupPascal, + grammarAgr, + ); + await writeFile( + path.join(actionsDir, `${group.name}ActionsSchema.agr`), + groupGrammarContent, + ); + files.push(`src/actions/${group.name}ActionsSchema.agr`); + } + } + + // Stamp out manifest (with sub-action manifests if groups exist) + await writeFile( + path.join(srcDir, `${integrationName}Manifest.json`), + JSON.stringify( + buildManifest( + integrationName, + pascalName, + state.config.description ?? "", + pattern, + subGroups, + ), + null, + 2, + ), + ); + files.push(`src/${integrationName}Manifest.json`); + + // Stamp out handler + await writeFile( + path.join(srcDir, `${integrationName}ActionHandler.ts`), + buildHandler(integrationName, pascalName, pattern), + ); + files.push(`src/${integrationName}ActionHandler.ts`); + + // Stamp out package.json (with sub-schema build scripts if groups exist) + const subSchemaNames = subGroups?.map((g) => g.name); + await writeFile( + path.join(targetDir, "package.json"), + JSON.stringify( + buildPackageJson( + integrationName, + packageName, + pascalName, + pattern, + subSchemaNames, + ), + null, + 2, + ), + ); + files.push(`package.json`); + + // Stamp out tsconfigs + await writeFile( + path.join(targetDir, "tsconfig.json"), + JSON.stringify(ROOT_TSCONFIG, null, 2), + ); + await writeFile( + path.join(srcDir, "tsconfig.json"), + JSON.stringify(SRC_TSCONFIG, null, 2), + ); + files.push(`tsconfig.json`, `src/tsconfig.json`); + + // Also copy to workspace scaffolder dir for reference + await writeArtifact( + integrationName, + "scaffolder", + "scaffolded-to.txt", + targetDir, + ); + + await updatePhase(integrationName, "scaffolder", { status: "approved" }); + + let subSchemaNote = ""; + if (subGroups) { + subSchemaNote = + `\n\n**Sub-schemas generated:** ${subGroups.length} groups\n` + + subGroups + .map( + (g) => + `- **${g.name}** (${g.actions.length} actions): ${g.description}`, + ) + .join("\n"); + } + + return createActionResultFromMarkdownDisplay( + `## Agent scaffolded: ${integrationName}\n\n` + + `**Output directory:** \`${targetDir}\`\n\n` + + `**Files created:**\n` + + files.map((f) => `- \`${f}\``).join("\n") + + subSchemaNote + + `\n\n**Next step:** Phase 6 — use \`generateTests\` and \`runTests\` to validate.`, + ); +} + +// Build a sub-schema TypeScript file that re-exports only the actions belonging +// to this group. It imports from the main schema and creates a union type. +function buildSubSchemaTs( + _integrationName: string, + _pascalName: string, + group: SubSchemaGroup, + groupPascal: string, + fullSchemaTs: string, +): string { + // Extract individual action type names from the full schema that match the + // group's action list. TypeAgent schema files define types like: + // export type BoldAction = { actionName: "bold"; parameters: {...} }; + // and then a union: + // export type FooActions = BoldAction | ItalicAction | ...; + // + // We emit a new file that re-exports only the relevant action types and + // creates a new union type for this sub-schema group. + + const actionTypeNames = group.actions.map( + (a) => `${a.charAt(0).toUpperCase()}${a.slice(1)}Action`, + ); + + // Find action type blocks in the full schema that belong to this group + const actionBlocks: string[] = []; + for (const actionName of group.actions) { + // Match "export type XxxAction = ..." blocks + const typeName = `${actionName.charAt(0).toUpperCase()}${actionName.slice(1)}Action`; + const regex = new RegExp( + `(export\\s+type\\s+${typeName}\\s*=\\s*\\{[\\s\\S]*?\\};)`, + ); + const match = fullSchemaTs.match(regex); + if (match) { + actionBlocks.push(match[1]); + } + } + + const unionType = `export type ${groupPascal}Actions =\n | ${actionTypeNames.join("\n | ")};`; + + return `// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\n// Sub-schema: ${group.name} — ${group.description}\n// Auto-generated by the onboarding scaffolder.\n\n${actionBlocks.join("\n\n")}\n\n${unionType}\n`; +} + +// Build a sub-schema grammar file that includes only the rules relevant to +// this group's actions. +function buildSubSchemaAgr( + integrationName: string, + group: SubSchemaGroup, + groupPascal: string, + fullGrammarAgr: string, +): string { + // Grammar files contain rule blocks that typically start with the action name. + // We extract lines that reference actions in this group and build a new .agr. + const lines = fullGrammarAgr.split("\n"); + const relevantLines: string[] = []; + let inRelevantBlock = false; + const actionSet = new Set(group.actions); + + for (const line of lines) { + // Check if line starts a new action rule (e.g., "actionName:" or + // a line that contains an action name as an identifier) + const ruleMatch = line.match(/^(\w+)\s*:/); + if (ruleMatch) { + inRelevantBlock = actionSet.has(ruleMatch[1]); + } + + // Also include header/import lines (lines starting with '#' or 'from') + const isHeader = + line.startsWith("#") || + line.startsWith("from ") || + line.startsWith("//") || + line.trim() === ""; + + if (inRelevantBlock || isHeader) { + relevantLines.push(line); + } + } + + // Fix the schema file reference to point to the sub-schema + let content = relevantLines.join("\n"); + content = content.replace( + /from "\.\/[^"]*Schema\.ts"/g, + `from "./actions/${group.name}ActionsSchema.ts"`, + ); + // Update the schema type reference + content = content.replace( + /from "\.\/[^"]*"/g, + `from "./actions/${group.name}ActionsSchema.ts"`, + ); + + return `// Sub-schema grammar: ${group.name} — ${group.description}\n// Auto-generated by the onboarding scaffolder.\n\n${content}\n`; +} + +async function handleScaffoldPlugin( + integrationName: string, + template: string, + outputDir?: string, +): Promise { + const state = await loadState(integrationName); + if (!state) return { error: `Integration "${integrationName}" not found.` }; + + const templateInfo = PLUGIN_TEMPLATES[template]; + if (!templateInfo) { + return { + error: `Unknown template "${template}". Use listTemplates to see available templates.`, + }; + } + + const targetDir = + outputDir ?? + path.join(AGENTS_DIR, integrationName, templateInfo.defaultSubdir); + await fs.mkdir(targetDir, { recursive: true }); + + for (const [filename, content] of Object.entries( + templateInfo.files(integrationName), + )) { + await writeFile(path.join(targetDir, filename), content); + } + + return createActionResultFromMarkdownDisplay( + `## Plugin scaffolded: ${integrationName} (${template})\n\n` + + `**Output:** \`${targetDir}\`\n\n` + + `**Files created:**\n` + + Object.keys(templateInfo.files(integrationName)) + .map((f) => `- \`${f}\``) + .join("\n") + + `\n\n${templateInfo.nextSteps}`, + ); +} + +async function handleListTemplates(): Promise { + const lines = [ + `## Available scaffolding templates`, + ``, + `### Agent templates`, + `- **default** — TypeAgent agent package (manifest, handler, schema, grammar)`, + ``, + `### Plugin templates (use with \`scaffoldPlugin\`)`, + ...Object.entries(PLUGIN_TEMPLATES).map( + ([key, t]) => `- **${key}** — ${t.description}`, + ), + ]; + return createActionResultFromMarkdownDisplay(lines.join("\n")); +} + +// ─── Template helpers ──────────────────────────────────────────────────────── + +function toPascalCase(str: string): string { + return str + .split(/[-_\s]+/) + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(""); +} + +function buildManifest( + name: string, + pascalName: string, + description: string, + pattern: AgentPattern = "schema-grammar", + subGroups?: SubSchemaGroup[], +) { + const manifest: Record = { + emojiChar: "🔌", + description: description || `Agent for ${name}`, + defaultEnabled: false, + schema: { + description: `${pascalName} agent actions`, + originalSchemaFile: `./${name}Schema.ts`, + schemaFile: `../dist/${name}Schema.pas.json`, + grammarFile: `../dist/${name}Schema.ag.json`, + schemaType: `${pascalName}Actions`, + }, + }; + + // Pattern-specific manifest flags + if (pattern === "llm-streaming") { + manifest.injected = true; + manifest.cached = false; + manifest.streamingActions = ["generateResponse"]; + } else if (pattern === "view-ui") { + manifest.localView = true; + } + + if (subGroups && subGroups.length > 0) { + const subActionManifests: Record = {}; + for (const group of subGroups) { + const groupPascal = toPascalCase(group.name); + subActionManifests[group.name] = { + schema: { + description: group.description, + originalSchemaFile: `./actions/${group.name}ActionsSchema.ts`, + schemaFile: `../dist/actions/${group.name}ActionsSchema.pas.json`, + grammarFile: `../dist/actions/${group.name}ActionsSchema.ag.json`, + schemaType: `${groupPascal}Actions`, + }, + }; + } + manifest.subActionManifests = subActionManifests; + } + + return manifest; +} + +function buildHandler( + name: string, + pascalName: string, + pattern: AgentPattern = "schema-grammar", +): string { + switch (pattern) { + case "external-api": + return buildExternalApiHandler(name, pascalName); + case "llm-streaming": + return buildLlmStreamingHandler(name, pascalName); + case "sub-agent-orchestrator": + return buildSubAgentOrchestratorHandler(name, pascalName); + case "websocket-bridge": + return buildWebSocketBridgeHandler(name, pascalName); + case "state-machine": + return buildStateMachineHandler(name, pascalName); + case "native-platform": + return buildNativePlatformHandler(name, pascalName); + case "view-ui": + return buildViewUiHandler(name, pascalName); + case "command-handler": + return buildCommandHandlerTemplate(name, pascalName); + default: + return buildSchemaGrammarHandler(name, pascalName); + } +} + +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.\`, + ); +} +`; +} + +function buildPackageJson( + name: string, + packageName: string, + pascalName: string, + pattern: AgentPattern = "schema-grammar", + subSchemaNames?: string[], +) { + const scripts: Record = { + asc: `asc -i ./src/${name}Schema.ts -o ./dist/${name}Schema.pas.json -t ${pascalName}Actions`, + agc: `agc -i ./src/${name}Schema.agr -o ./dist/${name}Schema.ag.json`, + tsc: "tsc -b", + clean: "rimraf --glob dist *.tsbuildinfo *.done.build.log", + }; + + // Generate asc: and agc: scripts for each sub-schema + const buildTargets = ["npm:tsc", "npm:asc", "npm:agc"]; + if (subSchemaNames && subSchemaNames.length > 0) { + for (const groupName of subSchemaNames) { + const groupPascal = toPascalCase(groupName); + scripts[`asc:${groupName}`] = + `asc -i ./src/actions/${groupName}ActionsSchema.ts -o ./dist/actions/${groupName}ActionsSchema.pas.json -t ${groupPascal}Actions`; + scripts[`agc:${groupName}`] = + `agc -i ./src/actions/${groupName}ActionsSchema.agr -o ./dist/actions/${groupName}ActionsSchema.ag.json`; + buildTargets.push(`npm:asc:${groupName}`, `npm:agc:${groupName}`); + } + } + + scripts.build = `concurrently ${buildTargets.join(" ")}`; + + return { + name: packageName, + version: "0.0.1", + private: true, + description: `TypeAgent agent for ${name}`, + license: "MIT", + author: "Microsoft", + type: "module", + exports: { + "./agent/manifest": `./src/${name}Manifest.json`, + "./agent/handlers": `./dist/${name}ActionHandler.js`, + }, + scripts, + dependencies: { + "@typeagent/agent-sdk": "workspace:*", + ...(pattern === "llm-streaming" + ? { aiclient: "workspace:*", typechat: "workspace:*" } + : pattern === "external-api" + ? { aiclient: "workspace:*" } + : pattern === "websocket-bridge" + ? { ws: "^8.18.0" } + : {}), + }, + devDependencies: { + "@typeagent/action-schema-compiler": "workspace:*", + "action-grammar-compiler": "workspace:*", + concurrently: "^9.1.2", + rimraf: "^6.0.1", + typescript: "~5.4.5", + }, + }; +} + +const ROOT_TSCONFIG = { + extends: "../../../tsconfig.base.json", + compilerOptions: { composite: true }, + include: [], + references: [{ path: "./src" }], + "ts-node": { esm: true }, +}; + +const SRC_TSCONFIG = { + extends: "../../../../tsconfig.base.json", + compilerOptions: { composite: true, rootDir: ".", outDir: "../dist" }, + include: ["./**/*"], + "ts-node": { esm: true }, +}; + +const PLUGIN_TEMPLATES: Record< + string, + { + description: string; + defaultSubdir: string; + nextSteps: string; + files: (name: string) => Record; + } +> = { + "rest-client": { + description: "Simple REST API client bridge", + defaultSubdir: "src", + nextSteps: + "Implement `executeCommand(action, params)` to call your REST API endpoints.", + files: (name) => ({ + [`${name}Bridge.ts`]: buildRestClientTemplate(name), + }), + }, + "websocket-bridge": { + description: + "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.", + files: (name) => ({ + [`${name}Bridge.ts`]: buildWebSocketBridgeTemplate(name), + }), + }, + "office-addin": { + description: "Office.js task pane add-in skeleton", + defaultSubdir: "addin", + nextSteps: + "Load the add-in in Excel/Word/Outlook and configure the manifest URL.", + files: (name) => ({ + "taskpane.html": buildOfficeAddinHtml(name), + "taskpane.ts": buildOfficeAddinTs(name), + "manifest.xml": buildOfficeManifestXml(name), + }), + }, +}; + +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; + } +} +`; +} + +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)); + }); + } +} +`; +} + +function buildOfficeAddinHtml(name: string): string { + return ` + + + + ${toPascalCase(name)} TypeAgent Add-in + + + + +

${toPascalCase(name)} TypeAgent

+
Connecting...
+ + +`; +} + +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}\`); +} +`; +} + +function buildOfficeManifestXml(name: string): string { + const pascal = toPascalCase(name); + return ` + + + 1.0.0.0 + Microsoft + en-US + + + + + + + + + ReadWriteDocument + +`; +} + +async function writeFile(filePath: string, content: string): Promise { + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, content, "utf-8"); +} + +// ─── Pattern listing ───────────────────────────────────────────────────────── + +async function handleListPatterns(): Promise { + const lines = [ + `## Agent architectural patterns`, + ``, + `Pass \`pattern\` to \`scaffoldAgent\` to generate pattern-appropriate boilerplate.`, + ``, + `| Pattern | When to use | Examples |`, + `|---------|-------------|----------|`, + `| \`schema-grammar\` | Standard: bounded set of typed actions (default) | weather, photo, list |`, + `| \`external-api\` | REST/OAuth cloud API (MS Graph, Spotify, GitHub…) | calendar, email, player |`, + `| \`llm-streaming\` | Agent calls an LLM and streams partial results | chat, greeting |`, + `| \`sub-agent-orchestrator\` | API surface too large for one schema; split into groups | desktop, code, browser |`, + `| \`websocket-bridge\` | Automate an app via a host-side plugin over WebSocket | browser, code |`, + `| \`state-machine\` | Multi-phase workflow with approval gates and disk persistence | onboarding, scriptflow |`, + `| \`native-platform\` | OS/device APIs via child_process or SDK; no cloud | androidMobile, playerLocal |`, + `| \`view-ui\` | Rich interactive UI rendered in a local web view | turtle, montage, markdown |`, + `| \`command-handler\` | Simple settings-style agent; direct dispatch, no schema | settings, test |`, + ]; + return createActionResultFromMarkdownDisplay(lines.join("\n")); +} + +// ─── 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, + 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: ActionContext, +): Promise { + // Optionally authenticate eagerly when the agent is enabled. +} + +async function executeAction( + action: TypeAgentAction<${pascalName}Actions>, + context: ActionContext, +): Promise { + const { client } = context.agentContext; + // TODO: map each action to a client.callApi() call. + return createActionResultFromTextDisplay( + \`Executing \${action.actionName} — not yet implemented.\`, + ); +} +`; +} + +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}\`, + ); + } +} +`; +} + +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 { ... } +`; +} + +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, + 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: ActionContext, +): Promise {} + +async function closeAgentContext(context: ActionContext): Promise { + await context.agentContext.bridge.stop(); +} + +async function executeAction( + action: TypeAgentAction<${pascalName}Actions>, + context: ActionContext, +): Promise { + const { bridge } = context.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) }; + } +} +`; +} + +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.\`, + ); +} +`; +} + +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}\`); + } +} +`; +} + +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, + 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: ActionContext, +): Promise { + if (enable) { + await context.sessionContext.agentIO.openLocalView( + context.sessionContext.requestId, + VIEW_PORT, + ); + } else { + await context.sessionContext.agentIO.closeLocalView( + context.sessionContext.requestId, + VIEW_PORT, + ); + } +} + +async function closeAgentContext(_context: ActionContext): 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.

\`, + ); +} +`; +} + +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); + }, + }; +} +`; +} diff --git a/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.agr b/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.agr new file mode 100644 index 0000000000..c355076f59 --- /dev/null +++ b/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.agr @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Grammar for Phase 5 — Scaffolder actions. + + = scaffold (the)? $(integrationName:wildcard) agent -> { + actionName: "scaffoldAgent", + parameters: { + integrationName + } +} + | (create | generate | stamp out) (the)? $(integrationName:wildcard) (agent)? (package)? -> { + actionName: "scaffoldAgent", + parameters: { + integrationName + } +} + | scaffold (the)? $(integrationName:wildcard) agent (using | with | as) (a | the)? schema grammar pattern -> { + actionName: "scaffoldAgent", + parameters: { integrationName, pattern: "schema-grammar" } +} + | scaffold (the)? $(integrationName:wildcard) agent (using | with | as) (a | the)? external api pattern -> { + actionName: "scaffoldAgent", + parameters: { integrationName, pattern: "external-api" } +} + | scaffold (the)? $(integrationName:wildcard) agent (using | with | as) (a | the)? (llm)? streaming pattern -> { + actionName: "scaffoldAgent", + parameters: { integrationName, pattern: "llm-streaming" } +} + | scaffold (the)? $(integrationName:wildcard) agent (using | with | as) (a | the)? (sub agent)? orchestrator pattern -> { + actionName: "scaffoldAgent", + parameters: { integrationName, pattern: "sub-agent-orchestrator" } +} + | scaffold (the)? $(integrationName:wildcard) agent (using | with | as) (a | the)? websocket (bridge)? pattern -> { + actionName: "scaffoldAgent", + parameters: { integrationName, pattern: "websocket-bridge" } +} + | scaffold (the)? $(integrationName:wildcard) agent (using | with | as) (a | the)? state machine pattern -> { + actionName: "scaffoldAgent", + parameters: { integrationName, pattern: "state-machine" } +} + | scaffold (the)? $(integrationName:wildcard) agent (using | with | as) (a | the)? native platform pattern -> { + actionName: "scaffoldAgent", + parameters: { integrationName, pattern: "native-platform" } +} + | scaffold (the)? $(integrationName:wildcard) agent (using | with | as) (a | the)? view (ui)? pattern -> { + actionName: "scaffoldAgent", + parameters: { integrationName, pattern: "view-ui" } +} + | scaffold (the)? $(integrationName:wildcard) agent (using | with | as) (a | the)? command handler pattern -> { + actionName: "scaffoldAgent", + parameters: { integrationName, pattern: "command-handler" } +}; + + = scaffold (the)? $(integrationName:wildcard) (plugin | extension) -> { + actionName: "scaffoldPlugin", + parameters: { + integrationName, + template: "rest-client" + } +} + | (create | generate) (the)? $(integrationName:wildcard) (plugin | extension) -> { + actionName: "scaffoldPlugin", + parameters: { + integrationName, + template: "rest-client" + } +}; + + = list (available)? templates -> { + actionName: "listTemplates", + parameters: {} +} + | (show | what are) (the)? (available)? (scaffolding)? templates -> { + actionName: "listTemplates", + parameters: {} +}; + + = list (available | agent)? patterns -> { + actionName: "listPatterns", + parameters: {} +} + | (show | what are) (the)? (available | agent)? (agent)? patterns -> { + actionName: "listPatterns", + parameters: {} +} + | what patterns (are)? (available | supported)? -> { + actionName: "listPatterns", + parameters: {} +}; + +import { ScaffolderActions } from "./scaffolderSchema.ts"; + + : ScaffolderActions = + | + | + | ; diff --git a/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.ts b/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.ts new file mode 100644 index 0000000000..6de7584c34 --- /dev/null +++ b/ts/packages/agents/onboarding/src/scaffolder/scaffolderSchema.ts @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Agent architectural patterns supported by the scaffolder. +export type AgentPattern = + | "schema-grammar" // Standard: schema + grammar + dispatch handler (default) + | "external-api" // REST/OAuth cloud API bridge (MS Graph, Spotify, etc.) + | "llm-streaming" // LLM-injected agent with streaming responses + | "sub-agent-orchestrator" // Root agent routing to N typed sub-schemas + | "websocket-bridge" // Bidirectional WebSocket to a host-side plugin + | "state-machine" // Multi-phase disk-persisted workflow + | "native-platform" // OS/device APIs via child_process or SDK + | "view-ui" // Web view renderer with IPC handler + | "command-handler"; // CommandHandler (direct dispatch, no typed schema) + +export type ScaffolderActions = + | ScaffoldAgentAction + | ScaffoldPluginAction + | ListTemplatesAction + | ListPatternsAction; + +export type ScaffoldAgentAction = { + actionName: "scaffoldAgent"; + parameters: { + // Integration name to scaffold agent for + integrationName: string; + // Architectural pattern to use (defaults to "schema-grammar") + pattern?: AgentPattern; + // Target directory for the agent package (defaults to ts/packages/agents/) + outputDir?: string; + }; +}; + +export type ScaffoldPluginAction = { + actionName: "scaffoldPlugin"; + parameters: { + // Integration name to scaffold the host-side plugin for + integrationName: string; + // Template to use for the plugin side + template: + | "office-addin" + | "vscode-extension" + | "electron-app" + | "browser-extension" + | "rest-client"; + // Target directory for the plugin (defaults to ts/packages/agents//plugin) + outputDir?: string; + }; +}; + +export type ListTemplatesAction = { + actionName: "listTemplates"; + parameters: {}; +}; + +export type ListPatternsAction = { + actionName: "listPatterns"; + parameters: {}; +}; diff --git a/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts b/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts new file mode 100644 index 0000000000..db58175b44 --- /dev/null +++ b/ts/packages/agents/onboarding/src/schemaGen/schemaGenHandler.ts @@ -0,0 +1,278 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Phase 3 — Schema Generation handler. +// Uses the approved API surface and generated phrases to produce a +// TypeScript action schema file with appropriate comments. + +import { + ActionContext, + TypeAgentAction, + ActionResult, +} from "@typeagent/agent-sdk"; +import { createActionResultFromMarkdownDisplay } from "@typeagent/agent-sdk/helpers/action"; +import { SchemaGenActions } from "./schemaGenSchema.js"; +import { + loadState, + updatePhase, + writeArtifact, + readArtifact, + readArtifactJson, +} from "../lib/workspace.js"; +import { getSchemaGenModel } from "../lib/llm.js"; +import { ApiSurface } from "../discovery/discoveryHandler.js"; +import { PhraseSet } from "../phraseGen/phraseGenHandler.js"; + +// Shared schema authoring guidelines injected into every schema gen/refine prompt. +const SCHEMA_GUIDELINES = ` +COMMENT STRUCTURE RULES: +1. Action-level block (above the action type declaration): use only for a short "what it does" description and example user/agent phrase pairs. No rules or constraints here. +2. Property-level comments (inside the parameters object, above each property declaration): ALL guidance lives here, co-located with the property it constrains. Do NOT put constraints at the action level. +3. No inline end-of-line comments on property declarations. All commentary goes in the line(s) above the property. + +PROPERTY COMMENT ORDERING (top = least important, bottom = most important — the LLM reads top-to-bottom, so put the critical constraint last, immediately before the property): +// General description of what this parameter is. +// Supplementary guidance / common aliases / optional tips. +// NOTE: or IMPORTANT: The hard constraint the model must not violate. +propertyName: type; + +CRITICAL CONSTRAINT FORMAT — embed a concrete WRONG/RIGHT example for any hard constraint; the WRONG case should be the exact failure mode you have observed: +// The data range in A1 notation. +// NOTE: Must be a literal cell range — do NOT use named ranges or structured references. +// WRONG: "SalesData[ActualSales]" ← structured table reference, will fail +// WRONG: "ActualSales" ← column name, will fail +// RIGHT: "C1:C7" ← literal A1 range +dataRange: string; + +BEST PRACTICES: +- Enum-like properties: always define the type as an explicit union of string literals instead of \`string\`. The comment above the property should name the underlying API enum it maps to and explain the default value and why. + Example: + // Label position relative to the data point. Maps to Office.js ChartDataLabelPosition enum. + // Default is "BestFit" — Office.js automatically chooses the best placement. + position?: "Top" | "Bottom" | "Center" | "InsideEnd" | "InsideBase" | "OutsideEnd" | "Left" | "Right" | "BestFit" | "Callout" | "None"; +`; + +export async function executeSchemaGenAction( + action: TypeAgentAction, + _context: ActionContext, +): Promise { + switch (action.actionName) { + case "generateSchema": + return handleGenerateSchema(action.parameters.integrationName); + + case "refineSchema": + return handleRefineSchema( + action.parameters.integrationName, + action.parameters.instructions, + ); + + case "approveSchema": + return handleApproveSchema(action.parameters.integrationName); + } +} + +async function handleGenerateSchema( + integrationName: string, +): Promise { + const state = await loadState(integrationName); + if (!state) return { error: `Integration "${integrationName}" not found.` }; + if (state.phases.discovery.status !== "approved") { + return { + error: `Discovery phase must be approved first. Run approveApiSurface.`, + }; + } + + const surface = await readArtifactJson( + integrationName, + "discovery", + "api-surface.json", + ); + if (!surface) { + return { + error: `Missing discovery artifact for "${integrationName}".`, + }; + } + // phraseSet is optional — we can still generate a schema without sample phrases + const phraseSet = await readArtifactJson( + integrationName, + "phraseGen", + "phrases.json", + ); + + await updatePhase(integrationName, "schemaGen", { status: "in-progress" }); + + const model = getSchemaGenModel(); + const prompt = buildSchemaPrompt( + integrationName, + surface, + phraseSet ?? null, + state.config.description, + ); + const result = await model.complete(prompt); + if (!result.success) { + return { error: `Schema generation failed: ${result.message}` }; + } + + const schemaTs = extractTypeScript(result.data); + await writeArtifact(integrationName, "schemaGen", "schema.ts", schemaTs); + + return createActionResultFromMarkdownDisplay( + `## Schema generated: ${integrationName}\n\n` + + "```typescript\n" + + schemaTs.slice(0, 2000) + + (schemaTs.length > 2000 ? "\n// ... (truncated)" : "") + + "\n```\n\n" + + `Use \`refineSchema\` to adjust, or \`approveSchema\` to proceed to grammar generation.`, + ); +} + +async function handleRefineSchema( + integrationName: string, + instructions: string, +): Promise { + const existing = await readArtifact( + integrationName, + "schemaGen", + "schema.ts", + ); + if (!existing) { + return { + error: `No schema found for "${integrationName}". Run generateSchema first.`, + }; + } + + const model = getSchemaGenModel(); + const prompt = [ + { + role: "system" as const, + content: + "You are a TypeScript expert. Modify the given TypeAgent action schema according to the instructions. " + + "Preserve all copyright headers and existing structure.\n" + + SCHEMA_GUIDELINES + + "Respond in JSON format. Return a JSON object with a single `schema` key containing the updated TypeScript file content as a string.", + }, + { + role: "user" as const, + content: + `Refine this TypeAgent action schema for "${integrationName}".\n\n` + + `Instructions: ${instructions}\n\n` + + `Current schema:\n\`\`\`typescript\n${existing}\n\`\`\``, + }, + ]; + + const result = await model.complete(prompt); + if (!result.success) { + return { error: `Schema refinement failed: ${result.message}` }; + } + + const refined = extractTypeScript(result.data); + // Archive the previous version + const version = Date.now(); + await writeArtifact( + integrationName, + "schemaGen", + `schema.v${version}.ts`, + existing, + ); + await writeArtifact(integrationName, "schemaGen", "schema.ts", refined); + + return createActionResultFromMarkdownDisplay( + `## Schema refined: ${integrationName}\n\n` + + `Previous version archived as \`schema.v${version}.ts\`\n\n` + + "```typescript\n" + + refined.slice(0, 2000) + + (refined.length > 2000 ? "\n// ... (truncated)" : "") + + "\n```", + ); +} + +async function handleApproveSchema( + integrationName: string, +): Promise { + const schema = await readArtifact( + integrationName, + "schemaGen", + "schema.ts", + ); + if (!schema) { + return { + error: `No schema found for "${integrationName}". Run generateSchema first.`, + }; + } + + await updatePhase(integrationName, "schemaGen", { status: "approved" }); + + return createActionResultFromMarkdownDisplay( + `## Schema approved: ${integrationName}\n\n` + + `Schema saved to \`~/.typeagent/onboarding/${integrationName}/schemaGen/schema.ts\`\n\n` + + `**Next step:** Phase 4 — use \`generateGrammar\` to produce the .agr grammar file.`, + ); +} + +function buildSchemaPrompt( + integrationName: string, + surface: ApiSurface, + phraseSet: PhraseSet | null, + description?: string, +): { role: "system" | "user"; content: string }[] { + const actionSummary = surface.actions + .map((a) => { + const phrases = phraseSet?.phrases[a.name] ?? []; + return ( + `Action: ${a.name}\n` + + `Description: ${a.description}\n` + + (a.parameters?.length + ? `Parameters: ${a.parameters.map((p) => `${p.name}: ${p.type}${p.required ? "" : "?"}`).join(", ")}\n` + : "") + + (phrases.length + ? `Sample phrases:\n${phrases + .slice(0, 3) + .map((p) => ` - "${p}"`) + .join("\n")}` + : "") + ); + }) + .join("\n\n"); + + return [ + { + role: "system", + content: + "You are a TypeScript expert generating TypeAgent action schemas. " + + "TypeAgent action schemas are TypeScript union types where each member has an `actionName` discriminant and a `parameters` object. " + + "Follow these file-level conventions:\n" + + "- Start the file with:\n // Copyright (c) Microsoft Corporation.\n // Licensed under the MIT License.\n" + + "- Export a top-level union type named `Actions`\n" + + "- Each action type is named `Action`\n" + + '- Use `actionName: "camelCaseName"` as a string literal type\n' + + "- Parameters use camelCase names\n" + + "- Optional parameters use `?: type` syntax\n" + + SCHEMA_GUIDELINES + + "Respond in JSON format. Return a JSON object with a single `schema` key containing the TypeScript file content as a string.", + }, + { + role: "user", + content: + `Generate a TypeAgent action schema for the "${integrationName}" integration` + + (description ? ` (${description})` : "") + + `.\n\n` + + `Actions to include:\n\n${actionSummary}`, + }, + ]; +} + +function extractTypeScript(llmResponse: string): string { + // Try to parse as JSON first (when using json_object response format) + try { + const parsed = JSON.parse(llmResponse); + if (parsed.schema) return parsed.schema.trim(); + } catch { + // Not JSON, fall through to other extraction methods + } + // Strip markdown code fences if present + const fenceMatch = llmResponse.match( + /```(?:typescript|ts)?\n([\s\S]*?)```/, + ); + if (fenceMatch) return fenceMatch[1].trim(); + return llmResponse.trim(); +} diff --git a/ts/packages/agents/onboarding/src/schemaGen/schemaGenSchema.agr b/ts/packages/agents/onboarding/src/schemaGen/schemaGenSchema.agr new file mode 100644 index 0000000000..e5987f97c5 --- /dev/null +++ b/ts/packages/agents/onboarding/src/schemaGen/schemaGenSchema.agr @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Grammar for Phase 3 — Schema Generation actions. + +// generateSchema - produce TypeScript action schema from discovered actions + phrases + = generate (the)? (action)? schema for $(integrationName:wildcard) -> { + actionName: "generateSchema", + parameters: { + integrationName + } +} + | (create | produce | write) (the)? (typescript)? (action)? schema for $(integrationName:wildcard) -> { + actionName: "generateSchema", + parameters: { + integrationName + } +}; + +// refineSchema - update the schema based on instructions + = refine (the)? $(integrationName:wildcard) schema (to)? $(instructions:wildcard) -> { + actionName: "refineSchema", + parameters: { + integrationName, + instructions + } +} + | update (the)? $(integrationName:wildcard) schema (to)? $(instructions:wildcard) -> { + actionName: "refineSchema", + parameters: { + integrationName, + instructions + } +}; + +// approveSchema - lock in the schema + = approve (the)? $(integrationName:wildcard) schema -> { + actionName: "approveSchema", + parameters: { + integrationName + } +} + | (confirm | finalize) (the)? $(integrationName:wildcard) (action)? schema -> { + actionName: "approveSchema", + parameters: { + integrationName + } +}; + +import { SchemaGenActions } from "./schemaGenSchema.ts"; + + : SchemaGenActions = + | + | ; diff --git a/ts/packages/agents/onboarding/src/schemaGen/schemaGenSchema.ts b/ts/packages/agents/onboarding/src/schemaGen/schemaGenSchema.ts new file mode 100644 index 0000000000..397f9c17cc --- /dev/null +++ b/ts/packages/agents/onboarding/src/schemaGen/schemaGenSchema.ts @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export type SchemaGenActions = + | GenerateSchemaAction + | RefineSchemaAction + | ApproveSchemaAction; + +export type GenerateSchemaAction = { + actionName: "generateSchema"; + parameters: { + // Integration name to generate schema for + integrationName: string; + }; +}; + +export type RefineSchemaAction = { + actionName: "refineSchema"; + parameters: { + // Integration name + integrationName: string; + // Specific instructions for the LLM about what to change + // e.g. "make the listName parameter optional" or "add a sortOrder parameter to sortAction" + instructions: string; + }; +}; + +export type ApproveSchemaAction = { + actionName: "approveSchema"; + parameters: { + // Integration name to approve schema for + integrationName: string; + }; +}; diff --git a/ts/packages/agents/onboarding/src/testing/testingHandler.ts b/ts/packages/agents/onboarding/src/testing/testingHandler.ts new file mode 100644 index 0000000000..f182d7d80f --- /dev/null +++ b/ts/packages/agents/onboarding/src/testing/testingHandler.ts @@ -0,0 +1,649 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Phase 6 — Testing handler. +// Generates phrase→action test cases from the approved phrase set, +// runs them against the dispatcher using createDispatcher (same pattern +// as evalHarness.ts), and uses an LLM to propose repairs for failures. + +import { + ActionContext, + TypeAgentAction, + ActionResult, +} from "@typeagent/agent-sdk"; +import { + createActionResultFromTextDisplay, + createActionResultFromMarkdownDisplay, +} from "@typeagent/agent-sdk/helpers/action"; +import { TestingActions } from "./testingSchema.js"; +import { + loadState, + updatePhase, + writeArtifactJson, + readArtifactJson, + readArtifact, +} from "../lib/workspace.js"; +import { getTestingModel } from "../lib/llm.js"; +import { PhraseSet } from "../phraseGen/phraseGenHandler.js"; +import { createDispatcher } from "agent-dispatcher"; +import { + createNpmAppAgentProvider, + getFsStorageProvider, +} from "dispatcher-node-providers"; +import fs from "node:fs"; +import path from "node:path"; +import { getInstanceDir } from "agent-dispatcher/helpers/data"; +import type { + ClientIO, + IAgentMessage, + RequestId, + CommandResult, +} from "@typeagent/dispatcher-types"; +import type { + DisplayAppendMode, + DisplayContent, + MessageContent, +} from "@typeagent/agent-sdk"; + +export type TestCase = { + phrase: string; + expectedActionName: string; + // Expected parameter values (partial match is acceptable) + expectedParameters?: Record; +}; + +export type TestResult = { + phrase: string; + expectedActionName: string; + actualActionName?: string; + passed: boolean; + error?: string; +}; + +export type TestRun = { + integrationName: string; + ranAt: string; + total: number; + passed: number; + failed: number; + results: TestResult[]; +}; + +export type ProposedRepair = { + integrationName: string; + proposedAt: string; + // Suggested changes to the schema file + schemaChanges?: string; + // Suggested changes to the grammar file + grammarChanges?: string; + // Explanation of what was wrong and why these changes fix it + rationale: string; + applied?: boolean; + appliedAt?: string; +}; + +export async function executeTestingAction( + action: TypeAgentAction, + context: ActionContext, +): Promise { + switch (action.actionName) { + case "generateTests": + return handleGenerateTests(action.parameters.integrationName); + + case "runTests": + return handleRunTests( + action.parameters.integrationName, + context, // passed through for future session context use + action.parameters.forActions, + action.parameters.limit, + ); + + case "getTestResults": + return handleGetTestResults( + action.parameters.integrationName, + action.parameters.filter, + ); + + case "proposeRepair": + return handleProposeRepair( + action.parameters.integrationName, + action.parameters.forActions, + ); + + case "approveRepair": + return handleApproveRepair(action.parameters.integrationName); + } +} + +async function handleGenerateTests( + integrationName: string, +): Promise { + const state = await loadState(integrationName); + if (!state) return { error: `Integration "${integrationName}" not found.` }; + if (state.phases.scaffolder.status !== "approved") { + return { + error: `Scaffolder phase must be approved first. Run scaffoldAgent.`, + }; + } + + const phraseSet = await readArtifactJson( + integrationName, + "phraseGen", + "phrases.json", + ); + if (!phraseSet) { + return { error: `No phrases found for "${integrationName}".` }; + } + + await updatePhase(integrationName, "testing", { status: "in-progress" }); + + // Convert phrase set to test cases + const testCases: TestCase[] = []; + for (const [actionName, phrases] of Object.entries(phraseSet.phrases)) { + for (const phrase of phrases) { + testCases.push({ + phrase, + expectedActionName: actionName, + }); + } + } + + await writeArtifactJson( + integrationName, + "testing", + "test-cases.json", + testCases, + ); + + return createActionResultFromMarkdownDisplay( + `## Test cases generated: ${integrationName}\n\n` + + `**Total test cases:** ${testCases.length}\n` + + `**Actions covered:** ${Object.keys(phraseSet.phrases).length}\n\n` + + `Use \`runTests\` to execute them against the dispatcher.`, + ); +} + +async function handleRunTests( + integrationName: string, + _context: ActionContext, + forActions?: string[], + limit?: number, +): Promise { + const testCases = await readArtifactJson( + integrationName, + "testing", + "test-cases.json", + ); + if (!testCases || testCases.length === 0) { + return { + error: `No test cases found for "${integrationName}". Run generateTests first.`, + }; + } + + let toRun = forActions + ? testCases.filter((tc) => forActions.includes(tc.expectedActionName)) + : testCases; + if (limit) toRun = toRun.slice(0, limit); + + // Create a dispatcher and run each phrase through it. + // The scaffolded agent must be registered in config.json before running tests. + // Use `packageAgent --register` (phase 7) or add manually and restart TypeAgent. + let dispatcherSession: + | Awaited> + | undefined; + try { + dispatcherSession = await createTestDispatcher(); + } catch (err: any) { + return { + error: + `Failed to create dispatcher: ${err?.message ?? err}\n\n` + + `Make sure the "${integrationName}" agent is registered in config.json ` + + `and TypeAgent has been restarted. Run \`packageAgent --register\` first.`, + }; + } + + const results: TestResult[] = []; + for (const tc of toRun) { + const result = await runSingleTest( + tc, + integrationName, + dispatcherSession.dispatcher, + ); + results.push(result); + } + + await dispatcherSession.dispatcher.close(); + + const passed = results.filter((r) => r.passed).length; + const failed = results.length - passed; + + const testRun: TestRun = { + integrationName, + ranAt: new Date().toISOString(), + total: results.length, + passed, + failed, + results, + }; + + await writeArtifactJson( + integrationName, + "testing", + "results.json", + testRun, + ); + + const passRate = Math.round((passed / results.length) * 100); + + const failingSummary = results + .filter((r) => !r.passed) + .slice(0, 10) + .map( + (r) => + `- ❌ "${r.phrase}" → expected \`${r.expectedActionName}\`, got \`${r.actualActionName ?? "error"}\`${r.error ? ` (${r.error})` : ""}`, + ) + .join("\n"); + + return createActionResultFromMarkdownDisplay( + `## Test results: ${integrationName}\n\n` + + `**Pass rate:** ${passRate}% (${passed}/${results.length})\n\n` + + (failed > 0 + ? `**Failing tests (first 10):**\n${failingSummary}\n\n` + + `Use \`proposeRepair\` to get LLM-suggested schema/grammar fixes.` + : `All tests passed! Use \`approveRepair\` to finalize or proceed to packaging.`), + ); +} + +async function handleGetTestResults( + integrationName: string, + filter?: "passing" | "failing", +): Promise { + const testRun = await readArtifactJson( + integrationName, + "testing", + "results.json", + ); + if (!testRun) { + return { + error: `No test results found for "${integrationName}". Run runTests first.`, + }; + } + + const results = filter + ? testRun.results.filter((r) => + filter === "passing" ? r.passed : !r.passed, + ) + : testRun.results; + + const lines = [ + `## Test results: ${integrationName}`, + ``, + `**Run at:** ${testRun.ranAt}`, + `**Total:** ${testRun.total} | **Passed:** ${testRun.passed} | **Failed:** ${testRun.failed}`, + ``, + `| Result | Phrase | Expected | Actual |`, + `|---|---|---|---|`, + ...results + .slice(0, 50) + .map( + (r) => + `| ${r.passed ? "✅" : "❌"} | "${r.phrase}" | \`${r.expectedActionName}\` | \`${r.actualActionName ?? r.error ?? "—"}\` |`, + ), + ]; + if (results.length > 50) { + lines.push(``, `_...and ${results.length - 50} more_`); + } + + return createActionResultFromMarkdownDisplay(lines.join("\n")); +} + +async function handleProposeRepair( + integrationName: string, + forActions?: string[], +): Promise { + const testRun = await readArtifactJson( + integrationName, + "testing", + "results.json", + ); + if (!testRun) { + return { error: `No test results found. Run runTests first.` }; + } + + const failing = testRun.results.filter((r) => !r.passed); + if (failing.length === 0) { + return createActionResultFromTextDisplay( + "All tests are passing — no repairs needed.", + ); + } + + const schemaTs = await readArtifact( + integrationName, + "schemaGen", + "schema.ts", + ); + const grammarAgr = await readArtifact( + integrationName, + "grammarGen", + "schema.agr", + ); + + const filteredFailing = forActions + ? failing.filter((r) => forActions.includes(r.expectedActionName)) + : failing; + + const model = getTestingModel(); + const prompt = buildRepairPrompt( + integrationName, + filteredFailing, + schemaTs ?? "", + grammarAgr ?? "", + ); + + const result = await model.complete(prompt); + if (!result.success) { + return { error: `Repair proposal failed: ${result.message}` }; + } + + // Try to parse as JSON first (when using json_object response format) + let responseText = result.data; + let schemaFromJson: string | undefined; + let grammarFromJson: string | undefined; + try { + const parsed = JSON.parse(result.data); + responseText = parsed.explanation || result.data; + schemaFromJson = parsed.schema; + grammarFromJson = parsed.grammar; + } catch { + // Not JSON, fall through to regex extraction + } + + const repair: ProposedRepair = { + integrationName, + proposedAt: new Date().toISOString(), + rationale: responseText, + }; + + // Extract suggested schema and grammar changes from the response + const schemaMatch = schemaFromJson + ? null + : result.data.match(/```typescript([\s\S]*?)```/); + const grammarMatch = grammarFromJson + ? null + : result.data.match(/```(?:agr)?([\s\S]*?)```/); + if (schemaFromJson) repair.schemaChanges = schemaFromJson.trim(); + else if (schemaMatch) repair.schemaChanges = schemaMatch[1].trim(); + if (grammarFromJson) repair.grammarChanges = grammarFromJson.trim(); + else if (grammarMatch) repair.grammarChanges = grammarMatch[1].trim(); + + await writeArtifactJson( + integrationName, + "testing", + "proposed-repair.json", + repair, + ); + + return createActionResultFromMarkdownDisplay( + `## Proposed repair: ${integrationName}\n\n` + + `**Failing tests addressed:** ${filteredFailing.length}\n\n` + + result.data.slice(0, 3000) + + (result.data.length > 3000 ? "\n\n_...truncated_" : "") + + `\n\nReview the proposed changes, then use \`approveRepair\` to apply them.`, + ); +} + +async function handleApproveRepair( + integrationName: string, +): Promise { + const repair = await readArtifactJson( + integrationName, + "testing", + "proposed-repair.json", + ); + if (!repair) { + return { error: `No proposed repair found. Run proposeRepair first.` }; + } + if (repair.applied) { + return createActionResultFromTextDisplay("Repair was already applied."); + } + + // Apply schema changes if present + if (repair.schemaChanges) { + const version = Date.now(); + const existing = await readArtifact( + integrationName, + "schemaGen", + "schema.ts", + ); + if (existing) { + await writeArtifactJson( + integrationName, + "testing", + `schema.backup.v${version}.ts`, + existing, + ); + } + const { writeArtifact } = await import("../lib/workspace.js"); + await writeArtifact( + integrationName, + "schemaGen", + "schema.ts", + repair.schemaChanges, + ); + } + + // Apply grammar changes if present + if (repair.grammarChanges) { + const version = Date.now(); + const existing = await readArtifact( + integrationName, + "grammarGen", + "schema.agr", + ); + if (existing) { + await writeArtifactJson( + integrationName, + "testing", + `grammar.backup.v${version}.agr`, + existing, + ); + } + const { writeArtifact } = await import("../lib/workspace.js"); + await writeArtifact( + integrationName, + "grammarGen", + "schema.agr", + repair.grammarChanges, + ); + } + + repair.applied = true; + repair.appliedAt = new Date().toISOString(); + await writeArtifactJson( + integrationName, + "testing", + "proposed-repair.json", + repair, + ); + + await updatePhase(integrationName, "testing", { status: "approved" }); + + return createActionResultFromMarkdownDisplay( + `## Repair applied: ${integrationName}\n\n` + + (repair.schemaChanges ? "- Schema updated\n" : "") + + (repair.grammarChanges ? "- Grammar updated\n" : "") + + `\nRe-run \`runTests\` to verify fixes, or \`packageAgent\` to proceed.`, + ); +} + +// ─── Dispatcher helpers ─────────────────────────────────────────────────────── + +// Minimal ClientIO that silently captures display output into a buffer. +// Mirrors the createCapturingClientIO pattern from evalHarness.ts. +function createCapturingClientIO(buffer: string[]): ClientIO { + const noop = (() => {}) as (...args: any[]) => any; + + function contentToText(content: DisplayContent): string { + if (typeof content === "string") return content; + if (Array.isArray(content)) { + if (content.length === 0) return ""; + if (typeof content[0] === "string") + return (content as string[]).join("\n"); + return (content as string[][]).map((r) => r.join(" | ")).join("\n"); + } + // TypedDisplayContent + const msg = (content as any).content as MessageContent; + if (typeof msg === "string") return msg; + if (Array.isArray(msg)) return (msg as string[]).join("\n"); + return String(msg); + } + + return { + clear: noop, + exit: () => process.exit(0), + setUserRequest: noop, + setDisplayInfo: noop, + setDisplay(message: IAgentMessage) { + const text = contentToText(message.message); + if (text) buffer.push(text); + }, + appendDisplay(message: IAgentMessage, _mode: DisplayAppendMode) { + const text = contentToText(message.message); + if (text) buffer.push(text); + }, + appendDiagnosticData: noop, + setDynamicDisplay: noop, + askYesNo: async (_id: RequestId, _msg: string, def = false) => def, + proposeAction: async () => undefined, + popupQuestion: async () => { + throw new Error("popupQuestion not supported in test runner"); + }, + notify: noop, + openLocalView: async () => {}, + closeLocalView: async () => {}, + requestChoice: noop, + requestInteraction: noop, + interactionResolved: noop, + interactionCancelled: noop, + takeAction: noop, + } satisfies ClientIO; +} + +// Build a provider containing only the externally-registered agents. +// The scaffolded agent must be registered in the TypeAgent config before +// running tests (use `packageAgent --register` or add manually to config.json). +function getExternalAppAgentProviders(instanceDir: string) { + const configPath = path.join(instanceDir, "externalAgentsConfig.json"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const agents = fs.existsSync(configPath) + ? ((JSON.parse(fs.readFileSync(configPath, "utf8")) as any).agents ?? + {}) + : {}; + return [ + createNpmAppAgentProvider( + agents, + path.join(instanceDir, "externalagents/package.json"), + ), + ]; +} + +async function createTestDispatcher() { + const instanceDir = getInstanceDir(); + const appAgentProviders = getExternalAppAgentProviders(instanceDir); + const buffer: string[] = []; + const clientIO = createCapturingClientIO(buffer); + + const dispatcher = await createDispatcher("onboarding-test-runner", { + appAgentProviders, + agents: { commands: ["dispatcher"] }, + explainer: { enabled: false }, + cache: { enabled: false }, + collectCommandResult: true, + persistDir: instanceDir, + storageProvider: getFsStorageProvider(), + clientIO, + dblogging: false, + }); + + return { dispatcher, buffer }; +} + +async function runSingleTest( + tc: TestCase, + integrationName: string, + dispatcher: Awaited>["dispatcher"], +): Promise { + // Route to the specific integration agent: "@ " + const command = `@${integrationName} ${tc.phrase}`; + + let result: CommandResult | undefined; + try { + result = await dispatcher.processCommand(command); + } catch (err: any) { + return { + phrase: tc.phrase, + expectedActionName: tc.expectedActionName, + passed: false, + error: err?.message ?? String(err), + }; + } + + if (result?.lastError) { + return { + phrase: tc.phrase, + expectedActionName: tc.expectedActionName, + passed: false, + error: result.lastError, + }; + } + + // Check the first dispatched action's name against expected + const actualActionName = result?.actions?.[0]?.actionName; + const passed = actualActionName === tc.expectedActionName; + + return { + phrase: tc.phrase, + expectedActionName: tc.expectedActionName, + ...(actualActionName !== undefined ? { actualActionName } : undefined), + passed, + ...(passed + ? undefined + : { + error: `Expected "${tc.expectedActionName}", got "${actualActionName ?? "no action"}"`, + }), + }; +} + +function buildRepairPrompt( + integrationName: string, + failing: TestResult[], + schemaTs: string, + grammarAgr: string, +): { role: "system" | "user"; content: string }[] { + const failuresSummary = failing + .slice(0, 20) + .map( + (r) => + `Phrase: "${r.phrase}"\nExpected: ${r.expectedActionName}\nGot: ${r.actualActionName ?? r.error ?? "no match"}`, + ) + .join("\n\n"); + + return [ + { + role: "system", + content: + "You are a TypeAgent grammar and schema expert. Analyze failing phrase-to-action test cases " + + "and propose specific fixes to the TypeScript schema and/or .agr grammar file. " + + "Explain what is wrong and why your changes will fix it. " + + "Respond in JSON format. Return a JSON object with optional `schema` and `grammar` keys containing the updated file contents as strings, and an `explanation` key describing the fixes.", + }, + { + role: "user", + content: + `Fix the TypeAgent schema and grammar for "${integrationName}" to make these failing tests pass.\n\n` + + `Failing tests (${failing.length} total, showing first 20):\n\n${failuresSummary}\n\n` + + `Current schema:\n\`\`\`typescript\n${schemaTs.slice(0, 3000)}\n\`\`\`\n\n` + + `Current grammar:\n\`\`\`agr\n${grammarAgr.slice(0, 3000)}\n\`\`\``, + }, + ]; +} diff --git a/ts/packages/agents/onboarding/src/testing/testingSchema.agr b/ts/packages/agents/onboarding/src/testing/testingSchema.agr new file mode 100644 index 0000000000..5c6f4ed2ec --- /dev/null +++ b/ts/packages/agents/onboarding/src/testing/testingSchema.agr @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Grammar for Phase 6 — Testing actions. + + = generate tests for $(integrationName:wildcard) -> { + actionName: "generateTests", + parameters: { + integrationName + } +} + | (create | produce) (test cases | tests) for $(integrationName:wildcard) -> { + actionName: "generateTests", + parameters: { + integrationName + } +}; + + = run tests for $(integrationName:wildcard) -> { + actionName: "runTests", + parameters: { + integrationName + } +} + | (execute | run) (the)? $(integrationName:wildcard) tests -> { + actionName: "runTests", + parameters: { + integrationName + } +}; + + = (get | show | display) (the)? $(integrationName:wildcard) test results -> { + actionName: "getTestResults", + parameters: { + integrationName + } +} + | (what are | show me) (the)? (test)? results for $(integrationName:wildcard) -> { + actionName: "getTestResults", + parameters: { + integrationName + } +}; + + = propose (a)? repair for $(integrationName:wildcard) -> { + actionName: "proposeRepair", + parameters: { + integrationName + } +} + | (suggest | find) (a)? fix for (the)? $(integrationName:wildcard) (test)? failures -> { + actionName: "proposeRepair", + parameters: { + integrationName + } +}; + + = approve (the)? $(integrationName:wildcard) repair -> { + actionName: "approveRepair", + parameters: { + integrationName + } +} + | (apply | accept) (the)? $(integrationName:wildcard) (proposed)? (repair | fix) -> { + actionName: "approveRepair", + parameters: { + integrationName + } +}; + +import { TestingActions } from "./testingSchema.ts"; + + : TestingActions = + | + | + | + | ; diff --git a/ts/packages/agents/onboarding/src/testing/testingSchema.ts b/ts/packages/agents/onboarding/src/testing/testingSchema.ts new file mode 100644 index 0000000000..1b9e6e3788 --- /dev/null +++ b/ts/packages/agents/onboarding/src/testing/testingSchema.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export type TestingActions = + | GenerateTestsAction + | RunTestsAction + | GetTestResultsAction + | ProposeRepairAction + | ApproveRepairAction; + +export type GenerateTestsAction = { + actionName: "generateTests"; + parameters: { + // Integration name to generate tests for + integrationName: string; + }; +}; + +export type RunTestsAction = { + actionName: "runTests"; + parameters: { + // Integration name to run tests for + integrationName: string; + // Run only tests for these specific action names + forActions?: string[]; + // Maximum number of tests to run (runs all if omitted) + limit?: number; + }; +}; + +export type GetTestResultsAction = { + actionName: "getTestResults"; + parameters: { + // Integration name to get test results for + integrationName: string; + // Filter to show only passing or failing tests + filter?: "passing" | "failing"; + }; +}; + +export type ProposeRepairAction = { + actionName: "proposeRepair"; + parameters: { + // Integration name to propose repairs for + integrationName: string; + // If provided, propose repairs only for these specific failing action names + forActions?: string[]; + }; +}; + +export type ApproveRepairAction = { + actionName: "approveRepair"; + parameters: { + // Integration name to approve the proposed repair for + integrationName: string; + }; +}; diff --git a/ts/packages/agents/onboarding/src/tsconfig.json b/ts/packages/agents/onboarding/src/tsconfig.json new file mode 100644 index 0000000000..85efcd566d --- /dev/null +++ b/ts/packages/agents/onboarding/src/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "rootDir": ".", + "outDir": "../dist" + }, + "include": ["./**/*"], + "ts-node": { + "esm": true + } +} diff --git a/ts/packages/agents/onboarding/tsconfig.json b/ts/packages/agents/onboarding/tsconfig.json new file mode 100644 index 0000000000..acb9cb4a91 --- /dev/null +++ b/ts/packages/agents/onboarding/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true + }, + "include": [], + "references": [{ "path": "./src" }], + "ts-node": { + "esm": true + } +} diff --git a/ts/packages/defaultAgentProvider/data/config.json b/ts/packages/defaultAgentProvider/data/config.json index faea7bb6cc..b4ada610f7 100644 --- a/ts/packages/defaultAgentProvider/data/config.json +++ b/ts/packages/defaultAgentProvider/data/config.json @@ -67,6 +67,9 @@ }, "utility": { "name": "utility-typeagent" + }, + "onboarding": { + "name": "onboarding-agent" } }, "mcpServers": { diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml index a368486bf4..6e62a19304 100644 --- a/ts/pnpm-lock.yaml +++ b/ts/pnpm-lock.yaml @@ -2232,6 +2232,46 @@ importers: specifier: ^5.2.0 version: 5.2.1(debug@4.4.1)(webpack-cli@5.1.4)(webpack@5.105.0) + packages/agents/onboarding: + dependencies: + '@typeagent/agent-sdk': + specifier: workspace:* + version: link:../../agentSdk + '@typeagent/dispatcher-types': + specifier: workspace:* + version: link:../../dispatcher/types + agent-dispatcher: + specifier: workspace:* + version: link:../../dispatcher/dispatcher + aiclient: + specifier: workspace:* + version: link:../../aiclient + dispatcher-node-providers: + specifier: workspace:* + version: link:../../dispatcher/nodeProviders + typechat: + specifier: ^0.1.1 + version: 0.1.1(typescript@5.4.5)(zod@3.25.76) + devDependencies: + '@typeagent/action-schema-compiler': + specifier: workspace:* + version: link:../../actionSchemaCompiler + action-grammar-compiler: + specifier: workspace:* + version: link:../../actionGrammarCompiler + concurrently: + specifier: ^9.1.2 + version: 9.1.2 + prettier: + specifier: ^3.5.3 + version: 3.5.3 + rimraf: + specifier: ^6.0.1 + version: 6.0.1 + typescript: + specifier: ~5.4.5 + version: 5.4.5 + packages/agents/photo: dependencies: '@typeagent/agent-sdk': @@ -3364,6 +3404,9 @@ importers: music-local: specifier: workspace:* version: link:../agents/playerLocal + onboarding-agent: + specifier: workspace:* + version: link:../agents/onboarding photo-agent: specifier: workspace:* version: link:../agents/photo From 1242c6cd7c765f2eef412b7a28219d36d4cd7225 Mon Sep 17 00:00:00 2001 From: robgruen Date: Mon, 13 Apr 2026 17:50:19 -0700 Subject: [PATCH 28/33] Potential fix for pull request finding 'CodeQL / Bad HTML filtering regexp' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../agents/onboarding/src/discovery/discoveryHandler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts b/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts index 79faa7ccf7..a0b0a8e52f 100644 --- a/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts +++ b/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts @@ -216,8 +216,8 @@ function stripHtml(html: string): string { do { previous = sanitized; sanitized = sanitized - .replace(//gi, "") - .replace(//gi, ""); + .replace(/]*>[\s\S]*?<\/script\b[^>]*>/gi, "") + .replace(/]*>[\s\S]*?<\/style\b[^>]*>/gi, ""); } while (sanitized !== previous); return sanitized From 623e5064c930c9db2a4f3605fd287bea55ef7917 Mon Sep 17 00:00:00 2001 From: robgruen Date: Mon, 13 Apr 2026 17:51:10 -0700 Subject: [PATCH 29/33] Potential fix for pull request finding 'CodeQL / Incomplete multi-character sanitization' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts b/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts index a0b0a8e52f..7fbd1c96d0 100644 --- a/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts +++ b/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts @@ -223,8 +223,6 @@ function stripHtml(html: string): string { return sanitized .replace(/<[^>]+>/g, " ") .replace(/ /g, " ") - .replace(/</g, "<") - .replace(/>/g, ">") .replace(/"/g, '"') .replace(/&/g, "&") .replace(/\s{2,}/g, " ") From 23a2b7c87aafadc0d85ab3c776ea32395ee8cb00 Mon Sep 17 00:00:00 2001 From: robgruen Date: Mon, 13 Apr 2026 21:50:06 -0700 Subject: [PATCH 30/33] Potential fix for pull request finding 'CodeQL / Incomplete multi-character sanitization' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../onboarding/src/discovery/discoveryHandler.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts b/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts index 7fbd1c96d0..a5e4ecdcbc 100644 --- a/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts +++ b/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts @@ -220,8 +220,8 @@ function stripHtml(html: string): string { .replace(/]*>[\s\S]*?<\/style\b[^>]*>/gi, ""); } while (sanitized !== previous); - return sanitized - .replace(/<[^>]+>/g, " ") + // Decode common entities and normalize whitespace. + sanitized = sanitized .replace(/ /g, " ") .replace(/"/g, '"') .replace(/&/g, "&") @@ -229,6 +229,17 @@ function stripHtml(html: string): string { .trim(); } + + // Decode can re-introduce tag delimiters; sanitize again until stable. + do { + previous = sanitized; + sanitized = sanitized + .replace(/]*>[\s\S]*?<\/script\b[^>]*>/gi, "") + .replace(/]*>[\s\S]*?<\/style\b[^>]*>/gi, "") + .replace(/<[^>]+>/g, " "); + } while (sanitized !== previous); + + return sanitized.replace(/\s{2,}/g, " ").trim(); // Extract same-origin links from an HTML page. function extractLinks(baseUrl: string, html: string): string[] { const base = new URL(baseUrl); From 7bc1125a4b1a749f57e40be9debada740e8b7b63 Mon Sep 17 00:00:00 2001 From: robgruen Date: Mon, 13 Apr 2026 21:56:17 -0700 Subject: [PATCH 31/33] Potential fix for pull request finding 'CodeQL / Incomplete multi-character sanitization' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../src/discovery/discoveryHandler.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts b/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts index a5e4ecdcbc..f3975c4eb8 100644 --- a/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts +++ b/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts @@ -209,26 +209,23 @@ async function handleCrawlDocUrl( // Strip HTML tags and collapse whitespace to extract readable text. function stripHtml(html: string): string { - // Repeatedly remove multi-character patterns until stable to avoid - // incomplete sanitization from overlapping/re-formed substrings. let sanitized = html; let previous: string; + + // First pass: remove dangerous blocks and tags until stable. do { previous = sanitized; sanitized = sanitized .replace(/]*>[\s\S]*?<\/script\b[^>]*>/gi, "") - .replace(/]*>[\s\S]*?<\/style\b[^>]*>/gi, ""); + .replace(/]*>[\s\S]*?<\/style\b[^>]*>/gi, "") + .replace(/<[^>]+>/g, " "); } while (sanitized !== previous); - // Decode common entities and normalize whitespace. + // Decode common entities. sanitized = sanitized .replace(/ /g, " ") .replace(/"/g, '"') - .replace(/&/g, "&") - .replace(/\s{2,}/g, " ") - .trim(); -} - + .replace(/&/g, "&"); // Decode can re-introduce tag delimiters; sanitize again until stable. do { @@ -240,6 +237,8 @@ function stripHtml(html: string): string { } while (sanitized !== previous); return sanitized.replace(/\s{2,}/g, " ").trim(); +} + // Extract same-origin links from an HTML page. function extractLinks(baseUrl: string, html: string): string[] { const base = new URL(baseUrl); From a253e5b04fddf563b7cf6a905869b118222e336f Mon Sep 17 00:00:00 2001 From: robgruen Date: Mon, 13 Apr 2026 21:59:56 -0700 Subject: [PATCH 32/33] Potential fix for pull request finding 'CodeQL / Incomplete multi-character sanitization' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../agents/onboarding/src/discovery/discoveryHandler.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts b/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts index f3975c4eb8..8130098cb4 100644 --- a/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts +++ b/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts @@ -236,7 +236,8 @@ function stripHtml(html: string): string { .replace(/<[^>]+>/g, " "); } while (sanitized !== previous); - return sanitized.replace(/\s{2,}/g, " ").trim(); + // Final hardening: neutralize any remaining tag delimiters as single chars. + return sanitized.replace(/[<>]/g, " ").replace(/\s{2,}/g, " ").trim(); } // Extract same-origin links from an HTML page. From c06191407f4734cb6a744ace3135988eea9c9abc Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Mon, 13 Apr 2026 22:07:55 -0700 Subject: [PATCH 33/33] lint --- .../agents/onboarding/src/discovery/discoveryHandler.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts b/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts index 8130098cb4..ff70cf0351 100644 --- a/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts +++ b/ts/packages/agents/onboarding/src/discovery/discoveryHandler.ts @@ -237,7 +237,10 @@ function stripHtml(html: string): string { } while (sanitized !== previous); // Final hardening: neutralize any remaining tag delimiters as single chars. - return sanitized.replace(/[<>]/g, " ").replace(/\s{2,}/g, " ").trim(); + return sanitized + .replace(/[<>]/g, " ") + .replace(/\s{2,}/g, " ") + .trim(); } // Extract same-origin links from an HTML page.