@noodleseed/agent-kit 0.22.0 → 0.23.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 (32) hide show
  1. package/manifest.json +31 -31
  2. package/package.json +1 -1
  3. package/skills/claude-code/SKILL.md +1 -1
  4. package/skills/claude-code/examples/acme-bistro/src/server.ts +1 -1
  5. package/skills/claude-code/examples/acme-discovery/src/server.ts +1 -1
  6. package/skills/claude-code/examples/acme-tasks/src/server.ts +4 -2
  7. package/skills/claude-code/examples/acme-tasks/test/server.test.ts +9 -0
  8. package/skills/claude-code/examples/food-ordering/README.md +7 -3
  9. package/skills/claude-code/examples/food-ordering/src/helpers.ts +2 -0
  10. package/skills/claude-code/examples/food-ordering/src/server.ts +47 -2
  11. package/skills/claude-code/examples/food-ordering/src/views/ordering-flow.tsx +56 -3
  12. package/skills/claude-code/examples/food-ordering/test/server.test.ts +46 -0
  13. package/skills/claude-code/references/authoring-workflow.md +46 -1
  14. package/skills/claude-code/references/compile-errors.md +6 -1
  15. package/skills/claude-code/references/embedded-assistant.md +90 -9
  16. package/skills/claude-code/references/sdk-surface.md +2 -2
  17. package/skills/claude-code/references/widgets-and-apps.md +32 -7
  18. package/skills/codex/SKILL.md +1 -1
  19. package/skills/codex/examples/acme-bistro/src/server.ts +1 -1
  20. package/skills/codex/examples/acme-discovery/src/server.ts +1 -1
  21. package/skills/codex/examples/acme-tasks/src/server.ts +4 -2
  22. package/skills/codex/examples/acme-tasks/test/server.test.ts +9 -0
  23. package/skills/codex/examples/food-ordering/README.md +7 -3
  24. package/skills/codex/examples/food-ordering/src/helpers.ts +2 -0
  25. package/skills/codex/examples/food-ordering/src/server.ts +47 -2
  26. package/skills/codex/examples/food-ordering/src/views/ordering-flow.tsx +56 -3
  27. package/skills/codex/examples/food-ordering/test/server.test.ts +46 -0
  28. package/skills/codex/references/authoring-workflow.md +46 -1
  29. package/skills/codex/references/compile-errors.md +6 -1
  30. package/skills/codex/references/embedded-assistant.md +90 -9
  31. package/skills/codex/references/sdk-surface.md +2 -2
  32. package/skills/codex/references/widgets-and-apps.md +32 -7
package/manifest.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
- "packageVersion": "0.22.0",
2
+ "packageVersion": "0.23.0",
3
3
  "files": [
4
4
  {
5
5
  "path": "skills/codex/SKILL.md",
6
- "sha256": "854f9f274724ac7eeb5bdeb933f83b36cbcbeb3643e0de806ecc33fce89da832",
6
+ "sha256": "14feeaa845c5c982e020173b800ecfa825038ac7e5b6d6c242244204f2f478a9",
7
7
  "agentTarget": "codex"
8
8
  },
9
9
  {
10
10
  "path": "skills/codex/references/sdk-surface.md",
11
- "sha256": "0916b1f4df01053c424b46eeb4e8ee0910709d66b956c4d19da77d5abc4b046b",
11
+ "sha256": "23b901198b1d6a2cb5c140810b2fc4d58b7008c2afb2fdd57a8e2993333a7600",
12
12
  "agentTarget": "codex"
13
13
  },
14
14
  {
@@ -23,17 +23,17 @@
23
23
  },
24
24
  {
25
25
  "path": "skills/codex/references/compile-errors.md",
26
- "sha256": "1039e245421e8c128cf1a81cf7a3ce809b0149478917040abebc124ffcb60423",
26
+ "sha256": "1ed667250f8f14625012bd8e931812c5d80bac323fa2f39a79ec83dc119fc385",
27
27
  "agentTarget": "codex"
28
28
  },
29
29
  {
30
30
  "path": "skills/codex/references/authoring-workflow.md",
31
- "sha256": "a2389c7cdbb28a817b8e5b616b625c02fed25fc017845349892c6e1b546a71c0",
31
+ "sha256": "3f4767bfe361090e172c7554f1121fe415e618f3a90304498535d177b0c633be",
32
32
  "agentTarget": "codex"
33
33
  },
34
34
  {
35
35
  "path": "skills/codex/references/embedded-assistant.md",
36
- "sha256": "e72fbea15298b980ccd876ced45f9e0b17b472859e1f5731283d7677ed9b0f13",
36
+ "sha256": "3e96c4f322cadbd509957781005060a8526c8e60e610bcb7246edc1e86349bf9",
37
37
  "agentTarget": "codex"
38
38
  },
39
39
  {
@@ -48,7 +48,7 @@
48
48
  },
49
49
  {
50
50
  "path": "skills/codex/references/widgets-and-apps.md",
51
- "sha256": "69ff229945465793e6c9ae10e5185fc3b5e59b9fb508f03a81ab8d2a12b39efc",
51
+ "sha256": "7e77eb1520883ad8bd0492fcefae97dfdb0e5bbe9351a7c554d19ee1f7c087cc",
52
52
  "agentTarget": "codex"
53
53
  },
54
54
  {
@@ -118,7 +118,7 @@
118
118
  },
119
119
  {
120
120
  "path": "skills/codex/examples/acme-bistro/src/server.ts",
121
- "sha256": "3f707b40fdf84ab2e8738367a2e0cccaf075a07aa945801ffb3a534f98ee1dcd",
121
+ "sha256": "35b735b5451499dad7b5bba9f4a0e0e57cc579d1ca8bcdd75317a044ed72c91b",
122
122
  "agentTarget": "codex"
123
123
  },
124
124
  {
@@ -173,7 +173,7 @@
173
173
  },
174
174
  {
175
175
  "path": "skills/codex/examples/acme-discovery/src/server.ts",
176
- "sha256": "3a8d1285b375b6e650db95033af0f1cb0c8f5468a6a454e4cbcc8aaae7f6ff9d",
176
+ "sha256": "0d5a6c50a126b379ffcc67b5089665e079f2aa5f7ddb1c6a5a99d6a6d37669e4",
177
177
  "agentTarget": "codex"
178
178
  },
179
179
  {
@@ -228,7 +228,7 @@
228
228
  },
229
229
  {
230
230
  "path": "skills/codex/examples/acme-tasks/src/server.ts",
231
- "sha256": "3876daf95af0e55d245e641b5e91a7cdd40bdccb6a4f0bd50c65a462ac98d37d",
231
+ "sha256": "769982ba567460af08d79517d6bf259ce4e8f7882cd3525fadbd5b22639e08ff",
232
232
  "agentTarget": "codex"
233
233
  },
234
234
  {
@@ -243,7 +243,7 @@
243
243
  },
244
244
  {
245
245
  "path": "skills/codex/examples/acme-tasks/test/server.test.ts",
246
- "sha256": "b170fb0963c21c804949dead018b9ce15deae0ecaa014abab19b4a7a05f42070",
246
+ "sha256": "1a6c729cceae3cf5ac5fea9081eb61c686a0a72c8a90539e5b0dee4307a4ff81",
247
247
  "agentTarget": "codex"
248
248
  },
249
249
  {
@@ -278,7 +278,7 @@
278
278
  },
279
279
  {
280
280
  "path": "skills/codex/examples/food-ordering/README.md",
281
- "sha256": "4d42dfbaf46ee8003ae84b3ef79c9d2a8b716943b85e2e9329fa875007019524",
281
+ "sha256": "1e8a08868efa70fe79bacc02d27effa5a65fbb5b497e76a7ce1268586ec559dc",
282
282
  "agentTarget": "codex"
283
283
  },
284
284
  {
@@ -293,12 +293,12 @@
293
293
  },
294
294
  {
295
295
  "path": "skills/codex/examples/food-ordering/src/helpers.ts",
296
- "sha256": "9d8a510ddfe3892340446dcadd6e40e3f88ea9f5b82ff780fc743771656f3e5b",
296
+ "sha256": "91f8dec747e4eb050c2e8dc8c22679443f22468c7fe7cf821e97f42eadb629e5",
297
297
  "agentTarget": "codex"
298
298
  },
299
299
  {
300
300
  "path": "skills/codex/examples/food-ordering/src/server.ts",
301
- "sha256": "6e8c4c17e6f3a7867232880e1a8d0e45428ebfd3938282d6bec7d8542ae7d712",
301
+ "sha256": "55942f7b6424ea6019479ab2df98afd0d5c33e154acddaf6638cf78d144da639",
302
302
  "agentTarget": "codex"
303
303
  },
304
304
  {
@@ -308,7 +308,7 @@
308
308
  },
309
309
  {
310
310
  "path": "skills/codex/examples/food-ordering/src/views/ordering-flow.tsx",
311
- "sha256": "81f8247c4a6370b5075279ac8cd54ddb669b5995c8fa385f4784720b99cb6203",
311
+ "sha256": "57690826caf1c7d4cd6da95a82fc8df3be1106f0de75716a3ccbb4dece2a953e",
312
312
  "agentTarget": "codex"
313
313
  },
314
314
  {
@@ -318,7 +318,7 @@
318
318
  },
319
319
  {
320
320
  "path": "skills/codex/examples/food-ordering/test/server.test.ts",
321
- "sha256": "e2f2fc0462073ba188c4a70251dafb029d7b3ae585275312c5fa55ba54127fc2",
321
+ "sha256": "6819457eed88b302d8369a24dc22581066d25c9092dca3a1141366665e246e52",
322
322
  "agentTarget": "codex"
323
323
  },
324
324
  {
@@ -378,12 +378,12 @@
378
378
  },
379
379
  {
380
380
  "path": "skills/claude-code/SKILL.md",
381
- "sha256": "6b0569348845fc943f50fe9a4ba408034316c9f8bf4867734b41bfba494f8310",
381
+ "sha256": "3f37fbed3225a691bac9b2f03a533d827479f01c6bfb3abf8b76c262af6ae7c2",
382
382
  "agentTarget": "claude-code"
383
383
  },
384
384
  {
385
385
  "path": "skills/claude-code/references/sdk-surface.md",
386
- "sha256": "0916b1f4df01053c424b46eeb4e8ee0910709d66b956c4d19da77d5abc4b046b",
386
+ "sha256": "23b901198b1d6a2cb5c140810b2fc4d58b7008c2afb2fdd57a8e2993333a7600",
387
387
  "agentTarget": "claude-code"
388
388
  },
389
389
  {
@@ -398,17 +398,17 @@
398
398
  },
399
399
  {
400
400
  "path": "skills/claude-code/references/compile-errors.md",
401
- "sha256": "1039e245421e8c128cf1a81cf7a3ce809b0149478917040abebc124ffcb60423",
401
+ "sha256": "1ed667250f8f14625012bd8e931812c5d80bac323fa2f39a79ec83dc119fc385",
402
402
  "agentTarget": "claude-code"
403
403
  },
404
404
  {
405
405
  "path": "skills/claude-code/references/authoring-workflow.md",
406
- "sha256": "a2389c7cdbb28a817b8e5b616b625c02fed25fc017845349892c6e1b546a71c0",
406
+ "sha256": "3f4767bfe361090e172c7554f1121fe415e618f3a90304498535d177b0c633be",
407
407
  "agentTarget": "claude-code"
408
408
  },
409
409
  {
410
410
  "path": "skills/claude-code/references/embedded-assistant.md",
411
- "sha256": "e72fbea15298b980ccd876ced45f9e0b17b472859e1f5731283d7677ed9b0f13",
411
+ "sha256": "3e96c4f322cadbd509957781005060a8526c8e60e610bcb7246edc1e86349bf9",
412
412
  "agentTarget": "claude-code"
413
413
  },
414
414
  {
@@ -423,7 +423,7 @@
423
423
  },
424
424
  {
425
425
  "path": "skills/claude-code/references/widgets-and-apps.md",
426
- "sha256": "69ff229945465793e6c9ae10e5185fc3b5e59b9fb508f03a81ab8d2a12b39efc",
426
+ "sha256": "7e77eb1520883ad8bd0492fcefae97dfdb0e5bbe9351a7c554d19ee1f7c087cc",
427
427
  "agentTarget": "claude-code"
428
428
  },
429
429
  {
@@ -493,7 +493,7 @@
493
493
  },
494
494
  {
495
495
  "path": "skills/claude-code/examples/acme-bistro/src/server.ts",
496
- "sha256": "3f707b40fdf84ab2e8738367a2e0cccaf075a07aa945801ffb3a534f98ee1dcd",
496
+ "sha256": "35b735b5451499dad7b5bba9f4a0e0e57cc579d1ca8bcdd75317a044ed72c91b",
497
497
  "agentTarget": "claude-code"
498
498
  },
499
499
  {
@@ -548,7 +548,7 @@
548
548
  },
549
549
  {
550
550
  "path": "skills/claude-code/examples/acme-discovery/src/server.ts",
551
- "sha256": "3a8d1285b375b6e650db95033af0f1cb0c8f5468a6a454e4cbcc8aaae7f6ff9d",
551
+ "sha256": "0d5a6c50a126b379ffcc67b5089665e079f2aa5f7ddb1c6a5a99d6a6d37669e4",
552
552
  "agentTarget": "claude-code"
553
553
  },
554
554
  {
@@ -603,7 +603,7 @@
603
603
  },
604
604
  {
605
605
  "path": "skills/claude-code/examples/acme-tasks/src/server.ts",
606
- "sha256": "3876daf95af0e55d245e641b5e91a7cdd40bdccb6a4f0bd50c65a462ac98d37d",
606
+ "sha256": "769982ba567460af08d79517d6bf259ce4e8f7882cd3525fadbd5b22639e08ff",
607
607
  "agentTarget": "claude-code"
608
608
  },
609
609
  {
@@ -618,7 +618,7 @@
618
618
  },
619
619
  {
620
620
  "path": "skills/claude-code/examples/acme-tasks/test/server.test.ts",
621
- "sha256": "b170fb0963c21c804949dead018b9ce15deae0ecaa014abab19b4a7a05f42070",
621
+ "sha256": "1a6c729cceae3cf5ac5fea9081eb61c686a0a72c8a90539e5b0dee4307a4ff81",
622
622
  "agentTarget": "claude-code"
623
623
  },
624
624
  {
@@ -653,7 +653,7 @@
653
653
  },
654
654
  {
655
655
  "path": "skills/claude-code/examples/food-ordering/README.md",
656
- "sha256": "4d42dfbaf46ee8003ae84b3ef79c9d2a8b716943b85e2e9329fa875007019524",
656
+ "sha256": "1e8a08868efa70fe79bacc02d27effa5a65fbb5b497e76a7ce1268586ec559dc",
657
657
  "agentTarget": "claude-code"
658
658
  },
659
659
  {
@@ -668,12 +668,12 @@
668
668
  },
669
669
  {
670
670
  "path": "skills/claude-code/examples/food-ordering/src/helpers.ts",
671
- "sha256": "9d8a510ddfe3892340446dcadd6e40e3f88ea9f5b82ff780fc743771656f3e5b",
671
+ "sha256": "91f8dec747e4eb050c2e8dc8c22679443f22468c7fe7cf821e97f42eadb629e5",
672
672
  "agentTarget": "claude-code"
673
673
  },
674
674
  {
675
675
  "path": "skills/claude-code/examples/food-ordering/src/server.ts",
676
- "sha256": "6e8c4c17e6f3a7867232880e1a8d0e45428ebfd3938282d6bec7d8542ae7d712",
676
+ "sha256": "55942f7b6424ea6019479ab2df98afd0d5c33e154acddaf6638cf78d144da639",
677
677
  "agentTarget": "claude-code"
678
678
  },
679
679
  {
@@ -683,7 +683,7 @@
683
683
  },
684
684
  {
685
685
  "path": "skills/claude-code/examples/food-ordering/src/views/ordering-flow.tsx",
686
- "sha256": "81f8247c4a6370b5075279ac8cd54ddb669b5995c8fa385f4784720b99cb6203",
686
+ "sha256": "57690826caf1c7d4cd6da95a82fc8df3be1106f0de75716a3ccbb4dece2a953e",
687
687
  "agentTarget": "claude-code"
688
688
  },
689
689
  {
@@ -693,7 +693,7 @@
693
693
  },
694
694
  {
695
695
  "path": "skills/claude-code/examples/food-ordering/test/server.test.ts",
696
- "sha256": "e2f2fc0462073ba188c4a70251dafb029d7b3ae585275312c5fa55ba54127fc2",
696
+ "sha256": "6819457eed88b302d8369a24dc22581066d25c9092dca3a1141366665e246e52",
697
697
  "agentTarget": "claude-code"
698
698
  },
699
699
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noodleseed/agent-kit",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "private": false,
5
5
  "description": "Self-checking, self-updating agent skills for the Noodle Seed CLI. Authored in this repo by @noodle-borg/agent-kit; this is the published, independently-versioned canonical skills artifact the CLI fetches and verifies.",
6
6
  "license": "Apache-2.0",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: noodle-seed
3
3
  description: Use when building, validating, testing, deploying, or operating a local or hosted Noodle Seed MCP server or app authored in TypeScript with the noodle CLI.
4
- version: 0.22.0
4
+ version: 0.23.0
5
5
  hash: b4fc528d406e2149
6
6
  ---
7
7
 
@@ -29,7 +29,7 @@ const menuItemOutput = z.object({
29
29
  // Tool annotations for host planners: the menu read is read-only; cart edits are local writes; checkout
30
30
  // opens an external (payment) link.
31
31
  const readOnly = annotations.readOnly();
32
- const localWrite = annotations.localAction({ destructive: false });
32
+ const localWrite = annotations.localAction({ destructive: false, confirm: false });
33
33
  const openLink = annotations.openAction();
34
34
 
35
35
  export default server(
@@ -83,7 +83,7 @@ const discoverInput = z.object({
83
83
  // shortlisting is a local non-destructive write.
84
84
  const readOnly = annotations.readOnly();
85
85
  const openLink = annotations.openAction();
86
- const localWrite = annotations.localAction({ destructive: false });
86
+ const localWrite = annotations.localAction({ destructive: false, confirm: false });
87
87
 
88
88
  const destinationOutput = z.object({
89
89
  id: z.string(),
@@ -28,6 +28,8 @@ const priority = z.enum(['high', 'medium', 'low']);
28
28
  // non-destructive writes.
29
29
  const readOnly = annotations.readOnly();
30
30
  const localWrite = annotations.localAction({ destructive: false });
31
+ const confirmedWrite = annotations.localAction({ destructive: false, confirm: true });
32
+ const widgetWrite = annotations.localAction({ destructive: false, confirm: false });
31
33
 
32
34
  const taskOutput = z.object({
33
35
  id: z.string(),
@@ -104,7 +106,7 @@ export default server(
104
106
  // Flow 3 — Complete: mark a task done. Model-visible so the model can complete on request.
105
107
  tool('complete_task', {
106
108
  description: 'Mark an Acme task complete. Pass the task id and its title.',
107
- annotations: localWrite,
109
+ annotations: confirmedWrite,
108
110
  input: z.object({
109
111
  task: z.string(),
110
112
  title: z.string().min(1),
@@ -122,7 +124,7 @@ export default server(
122
124
  tool('set_priority', {
123
125
  visibility: ['app'],
124
126
  description: 'Re-prioritize a task from the list widget.',
125
- annotations: localWrite,
127
+ annotations: widgetWrite,
126
128
  input: z.object({
127
129
  task: z.string(),
128
130
  priority,
@@ -19,4 +19,13 @@ describe('acme-tasks example', () => {
19
19
  const text = JSON.stringify(await app.toManifest());
20
20
  expect(text).toMatch(/"tasks":\[\{"id":"email_vendor".*"priority":"high"/);
21
21
  });
22
+
23
+ it('opts the conversational completion action into runtime confirmation', async () => {
24
+ const manifest = await app.toManifest();
25
+ const completeTask = manifest.tools.find((candidate) => candidate.name === 'complete_task');
26
+ const addTask = manifest.tools.find((candidate) => candidate.name === 'add_task');
27
+
28
+ expect(completeTask?.annotations?.confirm).toBe(true);
29
+ expect(addTask?.annotations).not.toHaveProperty('confirm');
30
+ });
22
31
  });
@@ -1,8 +1,9 @@
1
1
  # Food Ordering
2
2
 
3
3
  **Owns:** The flagship consumer ordering MCP App example: React view authoring, app-only helper tools,
4
- caller-scoped cart state handles, packaged image assets, checkout handoff policy, host actions,
5
- CSP/permissions metadata, and widget preview coverage.
4
+ caller-scoped cart state handles, invocation context, model-visible widget state/lifecycle, packaged image
5
+ assets, portable structured elicitation, checkout handoff policy, host actions, CSP/permissions metadata,
6
+ and widget preview coverage.
6
7
 
7
8
  Food Ordering is a generic, synthetic version of a live marketplace ordering app. It lets a user search
8
9
  stores, browse menus, customize an item, build a multi-line cart, review the order, and hand off checkout to
@@ -14,10 +15,13 @@ private customer data.
14
15
  | Capability | Example |
15
16
  | :--- | :--- |
16
17
  | Public entry tool | `open_ordering` returns structured fallback content and renders the React widget |
17
- | App-only helper tools | `search_stores`, `load_menu`, `load_item`, `read_cart`, `sync_cart`, `prepare_checkout` |
18
+ | App-only helper tools | `search_stores`, `load_menu`, `load_item`, `read_cart`, `sync_cart`, `prepare_checkout`; mutating widget-owned helpers use `confirm: false` (equivalent to omission) and execute directly because action hints alone never gate |
18
19
  | Durable cart state | `server(..., { state: { handles: { cart } }, use: { state } })` with caller scope and revision checks |
19
20
  | React app runtime kit | `@noodleseed/one/react` supplies app flow, shell/nav/view, async state, form, quantity, choice, and handoff primitives |
20
21
  | Multi-step widget flow | One React shell navigates stores, menu, item customization, cart, review, and handoff views through `useAppFlow` |
22
+ | Invocation context | `server.context` sets locale/time-zone defaults, derives an ambient service area/date, and makes the same snapshot available to tools and the reserved `noodle_context` MCP adapter |
23
+ | Structured missing input | `plan_order` uses `ctx.elicit` to collect a fulfilment method and date in embedded/headless renderers and bidirectional MCP adapters; the stateless hosted MCP endpoint does not yet carry the server-initiated exchange |
24
+ | Model-visible widget state | `useUpdateModelContext` publishes one cohesive replacement snapshot when supported; `useWidgetLifecycle` auto-publishes mounted/cancelled/dismissed and reports author-owned submitted milestones for future context (not host-presentation proof), while the user-triggered submit pairs `useSendFollowUpMessage` for an immediate reply |
21
25
  | Handoff | `handoff.allowedDomains` allows only `https://orders.example.com` checkout URLs |
22
26
  | Progressive enhancement | Non-Apps hosts still receive stores, featured items, and a readable fallback summary |
23
27
 
@@ -30,5 +30,7 @@ export const {
30
30
  useOpenExternal,
31
31
  useSendFollowUpMessage,
32
32
  useToolInfo,
33
+ useUpdateModelContext,
33
34
  useViewState,
35
+ useWidgetLifecycle,
34
36
  } = generateHelpers<AppType>();
@@ -174,7 +174,10 @@ type CartInput = {
174
174
  };
175
175
 
176
176
  const readOnly = annotations.readOnly();
177
- const action = annotations.openAction({ destructive: false });
177
+ // These writes are app-only controls inside the cart widget. The widget already presents the
178
+ // reviewed state and explicit button; `confirm: false` documents direct execution and is equivalent
179
+ // to omission because action/open-world hints alone never enable the confirmation gate.
180
+ const action = annotations.openAction({ destructive: false, confirm: false });
178
181
 
179
182
  function checkoutUrl(customer: string): string {
180
183
  return `https://orders.example.com/checkout?customer=${encodeURIComponent(customer)}`;
@@ -198,6 +201,16 @@ export default server(
198
201
  title: 'Food Ordering',
199
202
  version: '1.0.0',
200
203
  use: { state },
204
+ context: {
205
+ defaults: { locale: 'en-US', timeZone: 'America/New_York' },
206
+ ambient: {
207
+ output: z.object({ serviceArea: z.string(), orderingDate: z.string() }),
208
+ fulfil: ({ context }) => ({
209
+ serviceArea: 'Harbor District',
210
+ orderingDate: context.temporal.localDate,
211
+ }),
212
+ },
213
+ },
201
214
  state: {
202
215
  handles: {
203
216
  cart: {
@@ -239,13 +252,17 @@ export default server(
239
252
  customer: z.string(),
240
253
  stores: z.array(storeShape),
241
254
  featuredItems: z.array(menuItemShape),
255
+ localDate: z.string(),
256
+ serviceArea: z.string(),
242
257
  fallback: z.string(),
243
258
  }),
244
- fulfil: ({ input }) => ({
259
+ fulfil: ({ input, context }) => ({
245
260
  status: 'Ready to build a food order.',
246
261
  customer: input.customer,
247
262
  stores,
248
263
  featuredItems: menu,
264
+ localDate: context.temporal.localDate,
265
+ serviceArea: context.ambient.serviceArea,
249
266
  fallback: 'Open stores: Harbor Noodles (Noodles), Garden Wraps (Vegetarian).',
250
267
  }),
251
268
  viewTitle: 'Food ordering',
@@ -363,6 +380,34 @@ export default server(
363
380
  }),
364
381
  fulfil: () => ({ stores, featuredItems: menu }),
365
382
  }),
383
+ tool('plan_order', {
384
+ description:
385
+ 'Collect a fulfilment method and requested date as structured input, then return a reviewable order plan without placing an order.',
386
+ annotations: readOnly,
387
+ input: z.object({ customer: z.string().default('Guest') }),
388
+ output: z.object({
389
+ customer: z.string(),
390
+ method: z.enum(['pickup', 'delivery']),
391
+ requestedDate: z.string(),
392
+ serviceArea: z.string(),
393
+ }),
394
+ fulfil: ({ input, context, elicit }) => {
395
+ const preference = elicit({
396
+ id: 'choose_fulfilment',
397
+ message: 'How should we fulfil this order?',
398
+ input: z.object({
399
+ method: z.enum(['pickup', 'delivery']).describe('Fulfilment method'),
400
+ requestedDate: z.string().describe('Requested date').meta({ format: 'date' }),
401
+ }),
402
+ });
403
+ return {
404
+ customer: input.customer,
405
+ method: preference.method,
406
+ requestedDate: preference.requestedDate,
407
+ serviceArea: context.ambient.serviceArea,
408
+ };
409
+ },
410
+ }),
366
411
  tool('show_capabilities', {
367
412
  description: 'Return a concise summary for the standalone widget capability preview.',
368
413
  annotations: readOnly,
@@ -1,4 +1,4 @@
1
- import { useMemo, useState } from 'react';
1
+ import { useEffect, useMemo, useState } from 'react';
2
2
  import {
3
3
  ActionBar,
4
4
  AppShell,
@@ -19,7 +19,9 @@ import {
19
19
  useLayout,
20
20
  useSendFollowUpMessage,
21
21
  useToolInfo,
22
+ useUpdateModelContext,
22
23
  useViewState,
24
+ useWidgetLifecycle,
23
25
  View,
24
26
  ViewStack,
25
27
  } from '../helpers.js';
@@ -90,7 +92,7 @@ function modifierLabel(value: string): string {
90
92
  }
91
93
 
92
94
  export default function OrderingFlow() {
93
- const { displayMode, theme } = useLayout();
95
+ const { displayMode, supports, theme } = useLayout();
94
96
  const entry = structured<{
95
97
  readonly customer?: string;
96
98
  readonly stores?: readonly Store[];
@@ -106,6 +108,8 @@ export default function OrderingFlow() {
106
108
  const setView = flow.navigate;
107
109
  const handoff = useHandoff();
108
110
  const sendFollowUpMessage = useSendFollowUpMessage();
111
+ const updateModelContext = useUpdateModelContext();
112
+ const publishLifecycle = useWidgetLifecycle('ordering-flow');
109
113
  const searchStores = useCallTool('search_stores');
110
114
  const loadMenu = useCallTool('load_menu');
111
115
  const loadItem = useCallTool('load_item');
@@ -134,6 +138,7 @@ export default function OrderingFlow() {
134
138
  const setRevision = (value: number) => revisionStore.setState({ value });
135
139
  const [quantity, setQuantity] = useState(1);
136
140
  const [selectedModifiers, setSelectedModifiers] = useState<readonly string[]>([]);
141
+ const [lifecycle, setLifecycle] = useState<'active' | 'submitted'>('active');
137
142
 
138
143
  const storeData = structured<{ readonly stores?: readonly Store[] }>(searchStores.data);
139
144
  const menuData = structured<{
@@ -170,6 +175,37 @@ export default function OrderingFlow() {
170
175
  const llmSummary = `${view} view for ${customer}; ${cart.lines.length} cart lines; subtotal ${currency(
171
176
  cart.subtotal,
172
177
  )}`;
178
+ const canUpdateModelContext = supports?.modelContext === true;
179
+
180
+ useEffect(() => {
181
+ if (!canUpdateModelContext) return;
182
+ // Each call replaces model context, so publish one cohesive snapshot of current surface state.
183
+ void updateModelContext({
184
+ content: [{ type: 'text', text: `Food ordering: ${llmSummary}; ${lifecycle}.` }],
185
+ structuredContent: {
186
+ widget: { name: 'ordering-flow', lifecycle },
187
+ ordering: {
188
+ view,
189
+ customer,
190
+ cartLines: cart.lines.length,
191
+ subtotal: cart.subtotal,
192
+ selectedStoreId: selectedStoreId ?? null,
193
+ selectedItemId: selectedItemId ?? null,
194
+ },
195
+ },
196
+ });
197
+ }, [
198
+ canUpdateModelContext,
199
+ cart.lines.length,
200
+ cart.subtotal,
201
+ customer,
202
+ lifecycle,
203
+ llmSummary,
204
+ selectedItemId,
205
+ selectedStoreId,
206
+ updateModelContext,
207
+ view,
208
+ ]);
173
209
 
174
210
  async function chooseStore(store: Store) {
175
211
  setSelectedStoreId(store.id);
@@ -246,9 +282,26 @@ export default function OrderingFlow() {
246
282
  readonly revision?: number;
247
283
  readonly checkoutUrl?: string;
248
284
  }>(result);
249
- setCart(prepared?.cart ?? cart);
285
+ const preparedCart = prepared?.cart ?? cart;
286
+ setCart(preparedCart);
250
287
  setRevision(prepared?.revision ?? revision + 1);
288
+ setLifecycle('submitted');
251
289
  setView('handoff');
290
+ if (canUpdateModelContext) {
291
+ await publishLifecycle('submitted', {
292
+ view: 'handoff',
293
+ customer,
294
+ cartLines: preparedCart.lines.length,
295
+ subtotal: preparedCart.subtotal,
296
+ selectedStoreId: preparedCart.selectedStoreId ?? null,
297
+ selectedItemId: selectedItemId ?? null,
298
+ });
299
+ }
300
+ if (supports?.followUpMessage) {
301
+ await sendFollowUpMessage({
302
+ prompt: 'Checkout is prepared. Confirm the cart summary and explain the final handoff.',
303
+ });
304
+ }
252
305
  }
253
306
 
254
307
  return (
@@ -10,6 +10,13 @@ describe('food-ordering example', () => {
10
10
  const manifest = (await app.toManifest()) as {
11
11
  server: {
12
12
  name: string;
13
+ context?: {
14
+ defaults?: { locale?: string; timeZone?: string };
15
+ ambient?: {
16
+ outputSchema?: unknown;
17
+ fulfilment?: { output?: unknown };
18
+ };
19
+ };
13
20
  };
14
21
  handoff?: { allowedDomains?: string[] };
15
22
  state?: { handles?: Record<string, { kind: string; scope: string }> };
@@ -17,7 +24,9 @@ describe('food-ordering example', () => {
17
24
  tools: Array<{
18
25
  name: string;
19
26
  visibility?: string[];
27
+ annotations?: Record<string, unknown>;
20
28
  output?: unknown;
29
+ fulfilment?: { steps?: unknown[]; output?: unknown };
21
30
  }>;
22
31
  widgets?: Array<{
23
32
  name: string;
@@ -27,6 +36,17 @@ describe('food-ordering example', () => {
27
36
  };
28
37
 
29
38
  expect(manifest.server.name).toBe('food_ordering');
39
+ expect(manifest.server.context).toMatchObject({
40
+ defaults: { locale: 'en-US', timeZone: 'America/New_York' },
41
+ ambient: {
42
+ fulfilment: {
43
+ output: {
44
+ serviceArea: 'Harbor District',
45
+ orderingDate: '${context.temporal.localDate}',
46
+ },
47
+ },
48
+ },
49
+ });
30
50
  expect(manifest.state?.handles?.cart).toMatchObject({
31
51
  kind: 'cart',
32
52
  scope: 'caller',
@@ -52,7 +72,33 @@ describe('food-ordering example', () => {
52
72
  expect(tools.get(helper)?.visibility).toEqual(['app']);
53
73
  }
54
74
  expect(JSON.stringify(tools.get('open_ordering'))).toContain('featuredItems');
75
+ expect(JSON.stringify(tools.get('open_ordering'))).toContain('${context.temporal.localDate}');
76
+ expect(JSON.stringify(tools.get('open_ordering'))).toContain('${context.ambient.serviceArea}');
55
77
  expect(JSON.stringify(tools.get('sync_cart'))).toContain('revision');
78
+ expect(tools.get('sync_cart')?.annotations?.confirm).toBe(false);
79
+ expect(tools.get('prepare_checkout')?.annotations?.confirm).toBe(false);
80
+ expect(tools.get('plan_order')?.fulfilment).toMatchObject({
81
+ steps: [
82
+ {
83
+ id: 'choose_fulfilment',
84
+ elicit: {
85
+ message: 'How should we fulfil this order?',
86
+ requestedSchema: {
87
+ type: 'object',
88
+ properties: {
89
+ method: { type: 'string', enum: ['pickup', 'delivery'] },
90
+ requestedDate: { type: 'string', format: 'date' },
91
+ },
92
+ required: ['method', 'requestedDate'],
93
+ },
94
+ },
95
+ },
96
+ ],
97
+ output: {
98
+ method: '${steps.choose_fulfilment.method}',
99
+ requestedDate: '${steps.choose_fulfilment.requestedDate}',
100
+ },
101
+ });
56
102
  expect(manifest.widgets?.map((widget) => widget.name)).toContain('capabilities_card');
57
103
  });
58
104
  });