@noodleseed/agent-kit 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/manifest.json +49 -49
  2. package/package.json +1 -1
  3. package/skills/claude-code/SKILL.md +1 -1
  4. package/skills/claude-code/examples/acme-bistro/README.md +1 -1
  5. package/skills/claude-code/examples/acme-bistro/design/UX-Document.md +4 -4
  6. package/skills/claude-code/examples/acme-bistro/design/api-contract.md +3 -3
  7. package/skills/claude-code/examples/acme-bistro/design/wireframe.html +3 -3
  8. package/skills/claude-code/examples/acme-bistro/src/server.ts +8 -6
  9. package/skills/claude-code/examples/acme-discovery/README.md +1 -1
  10. package/skills/claude-code/examples/acme-discovery/design/UX-Document.md +3 -3
  11. package/skills/claude-code/examples/acme-discovery/design/wireframe.html +2 -2
  12. package/skills/claude-code/examples/acme-discovery/src/server.ts +6 -5
  13. package/skills/claude-code/examples/acme-tasks/README.md +30 -1
  14. package/skills/claude-code/examples/acme-tasks/design/UX-Document.md +2 -2
  15. package/skills/claude-code/examples/acme-tasks/design/wireframe.html +6 -6
  16. package/skills/claude-code/examples/acme-tasks/src/server.ts +6 -5
  17. package/skills/claude-code/examples/food-ordering/src/server.ts +46 -56
  18. package/skills/claude-code/examples/weather/src/server.ts +29 -41
  19. package/skills/claude-code/references/authoring-workflow.md +15 -10
  20. package/skills/claude-code/references/cli-commands.md +1 -0
  21. package/skills/claude-code/references/compile-errors.md +2 -2
  22. package/skills/claude-code/references/connect-an-api.md +2 -2
  23. package/skills/claude-code/references/experience-design.md +1 -1
  24. package/skills/claude-code/references/sdk-surface.md +16 -16
  25. package/skills/claude-code/references/troubleshooting.md +2 -2
  26. package/skills/claude-code/references/widgets-and-apps.md +9 -8
  27. package/skills/codex/SKILL.md +1 -1
  28. package/skills/codex/examples/acme-bistro/README.md +1 -1
  29. package/skills/codex/examples/acme-bistro/design/UX-Document.md +4 -4
  30. package/skills/codex/examples/acme-bistro/design/api-contract.md +3 -3
  31. package/skills/codex/examples/acme-bistro/design/wireframe.html +3 -3
  32. package/skills/codex/examples/acme-bistro/src/server.ts +8 -6
  33. package/skills/codex/examples/acme-discovery/README.md +1 -1
  34. package/skills/codex/examples/acme-discovery/design/UX-Document.md +3 -3
  35. package/skills/codex/examples/acme-discovery/design/wireframe.html +2 -2
  36. package/skills/codex/examples/acme-discovery/src/server.ts +6 -5
  37. package/skills/codex/examples/acme-tasks/README.md +30 -1
  38. package/skills/codex/examples/acme-tasks/design/UX-Document.md +2 -2
  39. package/skills/codex/examples/acme-tasks/design/wireframe.html +6 -6
  40. package/skills/codex/examples/acme-tasks/src/server.ts +6 -5
  41. package/skills/codex/examples/food-ordering/src/server.ts +46 -56
  42. package/skills/codex/examples/weather/src/server.ts +29 -41
  43. package/skills/codex/references/authoring-workflow.md +15 -10
  44. package/skills/codex/references/cli-commands.md +1 -0
  45. package/skills/codex/references/compile-errors.md +2 -2
  46. package/skills/codex/references/connect-an-api.md +2 -2
  47. package/skills/codex/references/experience-design.md +1 -1
  48. package/skills/codex/references/sdk-surface.md +16 -16
  49. package/skills/codex/references/troubleshooting.md +2 -2
  50. package/skills/codex/references/widgets-and-apps.md +9 -8
@@ -19,13 +19,13 @@ const geocoding = connector('open_meteo_geocoding')
19
19
  method: 'GET',
20
20
  path: '/v1/search',
21
21
  query: ['name'],
22
- input: { name: { type: 'string', required: true } },
23
- output: {
24
- latitude: { type: 'number', required: true },
25
- longitude: { type: 'number', required: true },
26
- place: { type: 'string' },
27
- country: { type: 'string' },
28
- },
22
+ input: z.object({ name: z.string() }),
23
+ output: z.object({
24
+ latitude: z.number(),
25
+ longitude: z.number(),
26
+ place: z.string().optional(),
27
+ country: z.string().optional(),
28
+ }),
29
29
  response: {
30
30
  latitude: '${response.results[0].latitude}',
31
31
  longitude: '${response.results[0].longitude}',
@@ -44,13 +44,8 @@ const geocoding = connector('open_meteo_geocoding')
44
44
  method: 'GET',
45
45
  path: '/v1/search',
46
46
  query: ['name', 'count'],
47
- input: {
48
- name: { type: 'string', required: true },
49
- count: { type: 'number' },
50
- },
51
- output: {
52
- results: { type: 'array' },
53
- },
47
+ input: z.object({ name: z.string(), count: z.number().optional() }),
48
+ output: z.object({ results: z.array(z.unknown()).optional() }),
54
49
  response: {
55
50
  results: '${response.results}',
56
51
  },
@@ -69,15 +64,12 @@ const forecast = connector('open_meteo_forecast')
69
64
  method: 'GET',
70
65
  path: '/v1/forecast?current_weather=true',
71
66
  query: ['latitude', 'longitude'],
72
- input: {
73
- latitude: { type: 'number', required: true },
74
- longitude: { type: 'number', required: true },
75
- },
76
- output: {
77
- temperature: { type: 'number' },
78
- windspeed: { type: 'number' },
79
- weathercode: { type: 'number' },
80
- },
67
+ input: z.object({ latitude: z.number(), longitude: z.number() }),
68
+ output: z.object({
69
+ temperature: z.number().optional(),
70
+ windspeed: z.number().optional(),
71
+ weathercode: z.number().optional(),
72
+ }),
81
73
  response: {
82
74
  temperature: '${response.current_weather.temperature}',
83
75
  windspeed: '${response.current_weather.windspeed}',
@@ -91,18 +83,18 @@ const brief = connector('weather_brief')
91
83
  .version('1.0.0')
92
84
  .compute('summarize', {
93
85
  type: 'read',
94
- input: {
95
- place: { type: 'string', required: true },
96
- country: { type: 'string' },
97
- temperature: { type: 'number', required: true },
98
- windspeed: { type: 'number', required: true },
99
- weathercode: { type: 'number', required: true },
100
- },
101
- output: {
102
- conditions: { type: 'string', required: true },
103
- headline: { type: 'string', required: true },
104
- advice: { type: 'string', required: true },
105
- },
86
+ input: z.object({
87
+ place: z.string(),
88
+ country: z.string().optional(),
89
+ temperature: z.number(),
90
+ windspeed: z.number(),
91
+ weathercode: z.number(),
92
+ }),
93
+ output: z.object({
94
+ conditions: z.string(),
95
+ headline: z.string(),
96
+ advice: z.string(),
97
+ }),
106
98
  // A real function — type-checked here, serialized to source and run in the sandbox. It must be
107
99
  // self-contained: no imports, no closure over outer variables, synchronous.
108
100
  run: (input) => {
@@ -162,12 +154,8 @@ const placeNarrow = connector('geo_places')
162
154
  .version('1.0.0')
163
155
  .compute('narrow', {
164
156
  type: 'read',
165
- input: {
166
- results: { type: 'unknown' },
167
- },
168
- output: {
169
- places: { type: 'array', required: true },
170
- },
157
+ input: z.object({ results: z.unknown().optional() }),
158
+ output: z.object({ places: z.array(z.unknown()) }),
171
159
  // Self-contained: no imports, no closure over outer variables, synchronous.
172
160
  run: (input) => {
173
161
  const raw = input.results;
@@ -11,6 +11,7 @@
11
11
  - Compute connector example
12
12
  - Tests
13
13
  - Secrets and variables
14
+ - Embedded assistant
14
15
  - Boundaries
15
16
 
16
17
  ## Input paths
@@ -55,16 +56,16 @@ const crm = connector('crm').version('1.0.0').http({
55
56
  method: 'GET',
56
57
  path: '/customers',
57
58
  query: ['email'],
58
- input: { email: { type: 'string', required: true } },
59
- output: { id: { type: 'string', required: true }, name: { type: 'string' } },
59
+ input: z.object({ email: z.string() }),
60
+ output: z.object({ id: z.string(), name: z.string().optional() }),
60
61
  response: { id: '${response.data[0].id}', name: '${response.data[0].name}' },
61
62
  },
62
63
  create_ticket: {
63
64
  type: 'action',
64
65
  method: 'POST',
65
66
  path: '/tickets',
66
- input: { customer_id: { type: 'string', required: true }, body: { type: 'string', required: true } },
67
- output: { ticket_id: { type: 'string', required: true } },
67
+ input: z.object({ customer_id: z.string(), body: z.string() }),
68
+ output: z.object({ ticket_id: z.string() }),
68
69
  request: { customer_id: '${args.customer_id}', body: '${args.body}' },
69
70
  response: { ticket_id: '${response.id}' },
70
71
  },
@@ -120,16 +121,16 @@ const tasks = connector('tasks').version('1.0.0').http({
120
121
  method: 'GET',
121
122
  path: '/tasks',
122
123
  query: ['query'],
123
- input: { query: { type: 'string', required: true } },
124
- output: { matches: { type: 'array', required: true } },
124
+ input: z.object({ query: z.string() }),
125
+ output: z.object({ matches: z.array(z.unknown()) }),
125
126
  response: { matches: '${response.results}' },
126
127
  },
127
128
  close_task: {
128
129
  type: 'action',
129
130
  method: 'POST',
130
131
  path: '/tasks/{id}/close',
131
- input: { id: { type: 'string', required: true } },
132
- output: { ok: { type: 'boolean', required: true } },
132
+ input: z.object({ id: z.string() }),
133
+ output: z.object({ ok: z.boolean() }),
133
134
  response: { ok: '${response.ok}' },
134
135
  },
135
136
  },
@@ -163,8 +164,8 @@ The model never sees a task id from the user; `find_tasks` returns `{ id, title
163
164
 
164
165
  ```ts
165
166
  const scoring = connector('scoring').version('1.0.0').compute('normalize', {
166
- input: { email: { type: "string", required: true }, priority: { type: "string" } },
167
- output: { score: { type: "number", required: true } },
167
+ input: z.object({ email: z.string(), priority: z.string().optional() }),
168
+ output: z.object({ score: z.number() }),
168
169
  calls: { find_customer: 'crm.find_customer' },
169
170
  limits: { timeoutMs: 1000, maxHostCalls: 2 },
170
171
  run(input, { callOperation }) {
@@ -197,6 +198,10 @@ After focused tests pass, run `noodle validate --json`, `noodle test --json`, an
197
198
 
198
199
  Author managed config as `secret("NAME")` / `variable("NAME")` and operate it with `noodle secrets set` / `noodle variables set` (scoped org/app/env). Never inline secret values in `server.ts`, tests, or generated files.
199
200
 
201
+ ## Embedded assistant
202
+
203
+ To place the same server tools inside the SaaS web app, add `assistant: embeddedAssistant({ model: openAICompatible({ baseUrl: variable("ASSISTANT_MODEL_BASE_URL"), model: variable("ASSISTANT_MODEL"), apiKey: secret("ASSISTANT_MODEL_API_KEY") }), allowedOrigins: ["https://app.example.com"], appearance: { brand: { name: "Acme Assistant" }, theme: { defaultMode: "auto", light: { accent: "#3157D5" }, dark: { accent: "#9FB4FF" } } } })` to the server options. Keep origins exact. The customer backend uses `@noodleseed/assistant/server` to exchange its already-authenticated user for a short-lived browser session; client and model secrets never enter the browser. Run `noodle check --target embedded-assistant` before deploy, then manage backend credentials with `noodle assistant clients create|list|rotate|revoke`.
204
+
200
205
  ## Boundaries
201
206
 
202
207
  Do not hand-author manifest JSON/YAML, runtime artifacts, connector IR, or hosted asset metadata. Do not read or copy secrets, bearer tokens, refresh tokens, static access keys, `.env.noodle`, or `~/.noodle/config.json`. Hosted access is identity-based — do not add static data-plane credential paths.
@@ -46,6 +46,7 @@ Every `noodle` command, grouped by area. Local authoring commands (`validate`, `
46
46
  | Command | What it does |
47
47
  | :-- | :-- |
48
48
  | `noodle link` | Bind this directory to a Noodle Seed Cloud target (org/app/env). |
49
+ | `noodle assistant` | Manage backend credentials for customer-branded embedded assistant clients. |
49
50
  | `noodle deploy` | Deploy the server to Noodle Seed Cloud. |
50
51
  | `noodle open` | Open or print the latest deployment URL. |
51
52
  | `noodle status` | Show hosted deployment status. |
@@ -38,8 +38,8 @@ Run `noodle validate` (add `--json` for the machine-readable envelope, `--fix-pr
38
38
  | `duplicate_prompt` | Two prompts share a name; rename one `prompt(...)`. |
39
39
  | `duplicate_resource_uri` | Two resources resolve to the same URI; make each resource URI unique. |
40
40
  | `unsupported_uri_template` | Fix the resource URI template to a supported form at the cited `path`. |
41
- | `duplicate_widget` | Two widgets share an identity; give each `widget(...)`/`view` a unique component name. |
42
- | `unknown_widget_tool` | The widget references a tool that does not exist; point `toolWithWidget`/`view` at a declared tool (see `didYouMean`). |
41
+ | `duplicate_widget` | Two tool views share an identity; give each `viewName` or `view.component` a unique name. |
42
+ | `unknown_widget_tool` | The widget references a tool that does not exist; point `tool`/`view` at a declared tool (see `didYouMean`). |
43
43
  | `unknown_widget_action_tool` | A widget action calls a tool that is not declared; declare it or fix the action target name. |
44
44
  | `duplicate_widget_tool` | A tool is bound to more than one widget; bind each tool to a single widget. |
45
45
  | `invalid_widget_binding` | Fix the `data-bind`/binding expression in the widget; it does not resolve against the tool output. |
@@ -62,7 +62,7 @@ Most real tools return a variable-length list (search results, a user’s tasks)
62
62
 
63
63
  ```ts
64
64
  // The API returns { results: [ { id, name, country, … }, … ] }
65
- output: { places: { type: 'array' } },
65
+ output: z.object({ places: z.array(z.unknown()) }),
66
66
  response: { places: '${response.results}' },
67
67
  ```
68
68
 
@@ -96,7 +96,7 @@ Pair the read/list with the mutations your intent tools need:
96
96
  ```ts
97
97
  close_task: {
98
98
  type: 'action', method: 'POST', path: '/tasks/{id}/close',
99
- input: { id: { type: 'string', required: true } },
99
+ input: z.object({ id: z.string() }),
100
100
  responseType: 'empty',
101
101
  },
102
102
  ```
@@ -87,7 +87,7 @@ the widget**, and the widget filled with plausible, internally consistent data (
87
87
  Label each widget with its component name so the wireframe, the spec, and the code share one
88
88
  vocabulary; put the funnel boundary at the top; and render off-app destinations distinctly (they are
89
89
  reached only after the handoff). Each wireframe screen maps directly to noodle: a screen with a widget
90
- is a `toolWithWidget` + a React `view`; a plain answer is a `tool`; an off-app destination is a
90
+ is a `tool` + a React `view`; a plain answer is a `tool`; an off-app destination is a
91
91
  `handoff.allowedDomains` entry. A compact single screen, anonymized to a fictional "Acme" business:
92
92
 
93
93
  ```html
@@ -14,13 +14,10 @@ Platform helper connectors are explicit subpath imports from `@noodleseed/one/pl
14
14
  ### Server & tools
15
15
 
16
16
  - `server(name, options, definitions)` — the server/app root.
17
- - `tool(name, { description, input, output, fulfil })` a model-visible tool.
18
- - `toolWithWidget(name, { ..., view })` — a model-visible tool that renders an MCP Apps widget.
19
- - `toolForWidget(name, { ... })` — a widget-only helper tool, hidden from the model.
17
+ - `tool(name, options)` declare every tool; add `view` to render an MCP App or `visibility: ["app"]` for an app-only helper.
20
18
 
21
- ### Widgets & assets
19
+ ### Views & assets
22
20
 
23
- - `widget(...)` — declare a widget/view component.
24
21
  - `asset("./path")` — reference a packaged asset (e.g. an image).
25
22
  - `annotations(...)` — tool/Apps annotation metadata.
26
23
 
@@ -51,14 +48,17 @@ Platform helper connectors are explicit subpath imports from `@noodleseed/one/pl
51
48
 
52
49
  - `z` — Zod, for input/output schemas (compiles to JSON Schema 2020-12).
53
50
 
51
+ ### Other
52
+
53
+ - `embeddedAssistant`
54
+ - `openAICompatible`
55
+
54
56
  ## Authoring signatures
55
57
 
56
- - `server(name, options, definitions)` — `options` commonly includes `title`, `version`, `instructions`, `branding`, `auth`, `use`, `provides`, `state`, and `handoff`; `definitions` is the array of tools/resources/prompts/widgets.
57
- - `tool(name, { description, input, output, annotations?, fulfil })` — `input`/`output` are Zod schemas; `fulfil({ input, connectors, user })` returns data matching `output`.
58
- - `toolWithWidget(name, { description, input, output, fulfil, view })` — same as `tool`, plus `view: { component, entry }` for a React widget.
59
- - `toolForWidget(name, { input, output, fulfil })` — helper tool for widget actions; hidden from the model.
58
+ - `server(name, options, definitions)` — `options` commonly includes `title`, `version`, `instructions`, `branding`, `auth`, `use`, `provides`, `state`, and `handoff`; `definitions` is the array of tools/resources/prompts.
59
+ - `tool(name, { description, input, output, annotations?, visibility?, view?, fulfil })` — `input`/`output` are Zod schemas; `fulfil({ input, connectors, user })` returns data matching `output`. Add `view: { component, entry }` for a React widget; use `visibility: ["app"]` for an app-only helper.
60
60
  - `resource(name, { uri, description?, mimeType?, fulfil })` and `prompt(name, { description?, arguments?, fulfil })` expose MCP resources/prompts.
61
- - `widget(name, { title, view, csp?, domain?, permissions? })` declares reusable view metadata; `asset("./path")` packages local files.
61
+ - View metadata (`viewTitle`, `viewDescription`, `csp`, `domain`, `permissions`) belongs on the tool that renders it; `asset("./path")` packages local files.
62
62
  - `customerAuth.*(...)` belongs in `server` options when deployed customer callers need verified identity; inspect `examples/customer-auth` or `examples/sharepoint` before using it.
63
63
  - `state` defines durable widget state handles; `handoff` declares allowed external domains for safe host handoff.
64
64
 
@@ -144,8 +144,8 @@ const crm = connector('crm')
144
144
  method: 'GET',
145
145
  path: '/tickets',
146
146
  query: ['id'],
147
- input: { id: { type: 'string', required: true } },
148
- output: { subject: { type: 'string' }, status: { type: 'string' } },
147
+ input: z.object({ id: z.string() }),
148
+ output: z.object({ subject: z.string().optional(), status: z.string().optional() }),
149
149
  response: { subject: '${response.subject}', status: '${response.status}' },
150
150
  },
151
151
  },
@@ -195,8 +195,8 @@ const orders = connector('orders')
195
195
  method: 'GET',
196
196
  path: '/orders',
197
197
  query: ['id'],
198
- input: { id: { type: 'string', required: true } },
199
- output: { id: { type: 'string' }, status: { type: 'string' } },
198
+ input: z.object({ id: z.string() }),
199
+ output: z.object({ id: z.string().optional(), status: z.string().optional() }),
200
200
  response: { id: '${response.id}', status: '${response.status}' },
201
201
  },
202
202
  get_tracking: {
@@ -204,8 +204,8 @@ const orders = connector('orders')
204
204
  method: 'GET',
205
205
  path: '/tracking',
206
206
  query: ['order_id'],
207
- input: { order_id: { type: 'string', required: true } },
208
- output: { url: { type: 'string' } },
207
+ input: z.object({ order_id: z.string() }),
208
+ output: z.object({ url: z.string().optional() }),
209
209
  response: { url: '${response.url}' },
210
210
  },
211
211
  },
@@ -19,9 +19,9 @@ For protocol/conformance checks, the headless harness is `@mcpjam/cli`, not a `n
19
19
  | ChatGPT warns “Widget CSP is not set” | The widget declares no `csp` | Declare `csp` on the widget with the exact origins it uses |
20
20
  | ChatGPT warns “Widget domain is not set” | No `domain` on the widget (required for app-store submission) | Set `domain: "https://…"` (one https origin per app) on each widget |
21
21
  | External links do nothing, or show a safe-link warning | Link opened outside the host bridge, or the target origin is not allowlisted | Use `useOpenExternal()` (never `window.open`) and add the target origins to the server-level `handoff.allowedDomains` |
22
- | Tool succeeds but no widget appears | The tool has no view, or the host surface doesn’t support MCP Apps | Use `toolWithWidget`, run `noodle check`, preview with `noodle devtools`; on non-Apps surfaces only the text/structured result renders |
22
+ | Tool succeeds but no widget appears | The tool has no view, or the host surface doesn’t support MCP Apps | Use `tool`, run `noodle check`, preview with `noodle devtools`; on non-Apps surfaces only the text/structured result renders |
23
23
  | Widget shows stale or missing data | The widget reads `structuredContent`, which must match the `output` schema | Make `fulfil` return exactly the `output` shape (arrays and nested objects are supported); inspect the live result with `noodle devtools` |
24
- | `useCallTool` fails from the widget | Tool name mismatch, or the helper tool is model-visible | List names with `noodle tools`; widget-only helpers must be declared with `toolForWidget` |
24
+ | `useCallTool` fails from the widget | Tool name mismatch, or the helper tool is model-visible | List names with `noodle tools`; widget-only helpers must be declared with `tool` |
25
25
  | `noodle validate` passes but React views fail to bundle (“requires Vite”) | Project dependencies are not installed — widget bundling uses the app-local Vite | Run the project’s package install, then retry `noodle dev` / `noodle deploy` |
26
26
  | Hosted endpoint returns 401 to probes | Expected: hosted servers challenge unauthenticated calls with OAuth metadata | Sign in from the host when prompted; widen who may call with `noodle access set` if testers are outside the org |
27
27
  | Tools error only after deploy | Runtime/config differences surface hosted (secrets, connector reachability) | Run `noodle smoke`, then `noodle metrics --agent-output` and `noodle events --tool <name> --status tool_error --json`; check `noodle secrets list` scope |
@@ -12,7 +12,7 @@
12
12
 
13
13
  ## Tools and views
14
14
 
15
- Use `toolWithWidget(name, { description, input, output, fulfil, view })` for a model-visible tool that renders a widget, and `toolForWidget(name, { ... })` for a widget-only helper hidden from the model. A `view` is `{ component: "name", entry: "./views/name.tsx" }` — a React component the compiler bundles at validate/deploy time.
15
+ Use `tool(name, { description, input, output, fulfil, view })` for a model-visible tool that renders a widget, and the same `tool(name, { ..., visibility: ["app"] })` for an app-only helper hidden from the model. A `view` is `{ component: "name", entry: "./views/name.tsx" }` — a React component the compiler bundles at validate/deploy time.
16
16
 
17
17
  ## React hook surface
18
18
 
@@ -21,7 +21,7 @@ Author views as React components. `generateHelpers<ServerDefinition>()` (from `@
21
21
  | Hook | Use for |
22
22
  | :-- | :-- |
23
23
  | `useToolInfo` | Read the invoking tool result; `structuredContent` is the widget’s typed data payload. |
24
- | `useCallTool` | Call a tool from the widget — returns `{ status, callTool, callToolAsync, data, structuredContent, error, reset }`; target a model-visible tool or a hidden `toolForWidget` helper. |
24
+ | `useCallTool` | Call a tool from the widget — returns `{ status, callTool, callToolAsync, data, structuredContent, error, reset }`; target a model-visible tool or a hidden `tool` helper. |
25
25
  | `useViewState` | Persist per-widget UI state across re-renders and restores: `const [value, setValue] = useViewState("key", initial)`. |
26
26
  | `useLayout` | Read host layout: `{ theme, displayMode, locale?, host?, supports? }` (`displayMode` is `"inline"`/`"pip"`/`"fullscreen"`) — adapt styling to the host theme and mode. |
27
27
  | `useRequestDisplayMode` | Request a host-mediated layout change such as fullscreen; treat it as best-effort and keep inline rendering useful. |
@@ -94,10 +94,10 @@ export default function OrderStatus() {
94
94
 
95
95
  ### 2. The tool declaration
96
96
 
97
- `toolWithWidget` is the model-visible tool that renders the view; pair it with `toolForWidget` helpers the view calls (hidden from the model). Wire `view: { component, entry }`, `csp`, a widget `domain`, and a real `output` schema so non-Apps hosts still receive structured data. Inside `fulfil`, `input` is a symbolic ref recorded into a flow — reference it in output/template strings, but never use it as an object key or `if` condition.
97
+ `tool` declares both the model-visible tool that renders the view and app-only helpers the view calls. Put `visibility: ["app"]` on each helper. Wire `view: { component, entry }`, `csp`, a widget `domain`, and a real `output` schema so non-Apps hosts still receive structured data. Inside `fulfil`, `input` is a symbolic ref recorded into a flow — reference it in output/template strings, but never use it as an object key or `if` condition.
98
98
 
99
99
  ```ts
100
- import { annotations, server, toolForWidget, toolWithWidget, z } from '@noodleseed/one';
100
+ import { annotations, server, tool, z } from '@noodleseed/one';
101
101
 
102
102
  const item = z.enum(['falafel_wrap', 'lentil_soup', 'mint_lemonade']).default('falafel_wrap');
103
103
  const checkoutUrl = (customer: string) =>
@@ -112,7 +112,7 @@ export default server(
112
112
  handoff: { allowedDomains: ['https://orders.example.com'] },
113
113
  },
114
114
  [
115
- toolWithWidget('show_order', {
115
+ tool('show_order', {
116
116
  description: 'Show the pickup order and render the ordering widget.',
117
117
  // Declare tool annotations — a ChatGPT-submission requirement. A read → `readOnly()`.
118
118
  annotations: annotations.readOnly(),
@@ -129,8 +129,8 @@ export default server(
129
129
  total: 12,
130
130
  checkoutUrl: checkoutUrl(input.customer),
131
131
  }),
132
- widgetTitle: 'Pickup order',
133
- widgetDescription: 'Pick an item and place a pickup order.',
132
+ viewTitle: 'Pickup order',
133
+ viewDescription: 'Pick an item and place a pickup order.',
134
134
  // A ChatGPT App is this widget + a domain: one https origin per app.
135
135
  domain: 'https://pickup.example.com',
136
136
  view: { component: 'order-status', entry: './views/order-status.tsx' },
@@ -142,7 +142,8 @@ export default server(
142
142
  },
143
143
  }),
144
144
  // Widget-only helper the view calls with useCallTool('place_order'); hidden from the model.
145
- toolForWidget('place_order', {
145
+ tool('place_order', {
146
+ visibility: ['app'],
146
147
  description: 'Place a pickup order from the widget.',
147
148
  // A write that reaches the outside world → `action()` (not read-only, not destructive).
148
149
  annotations: annotations.action(),