@datalayer/agent-runtimes 1.5.4 → 1.5.6

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.
@@ -99,7 +99,7 @@ export declare function uiProblems(app: Pick<AppSpec, 'ui' | 'enabled'>): string
99
99
  * know or said twice, a planned protocol while it is offered, and a `pap`,
100
100
  * `pact` or `a2a` block configuring a protocol it does not list.
101
101
  */
102
- export declare function protocolsProblems(app: Pick<AppSpec, 'protocols' | 'enabled' | 'pap' | 'pact' | 'a2a'>): string[];
102
+ export declare function protocolsProblems(app: Pick<AppSpec, 'protocols' | 'enabled' | 'pap' | 'pact' | 'a2a' | 'commerce'>): string[];
103
103
  /**
104
104
  * An application as the document its file holds.
105
105
  *
@@ -204,6 +204,7 @@ const KNOWN_KEYS = [
204
204
  'ui',
205
205
  'pap',
206
206
  'pact',
207
+ 'commerce',
207
208
  'a2a',
208
209
  'rules',
209
210
  'permissions',
@@ -333,6 +334,41 @@ function parsePact(data) {
333
334
  })),
334
335
  };
335
336
  }
337
+ /** Commerce (AGENT_PROTOCOLS PI-01): the merchants it shops at for each person. */
338
+ function parseCommerce(data) {
339
+ return {
340
+ merchants: records(data.merchants).map(merchant => ({
341
+ id: text(merchant.id),
342
+ protocol: oneOf(merchant.protocol, ['ucp', 'acp'], 'ucp'),
343
+ merchant: text(merchant.merchant),
344
+ access: oneOf(merchant.access, ['read', 'write'], 'read'),
345
+ ...(isData(merchant.limit)
346
+ ? {
347
+ limit: {
348
+ amount: text(merchant.limit.amount),
349
+ currency: text(merchant.limit.currency),
350
+ },
351
+ }
352
+ : {}),
353
+ })),
354
+ };
355
+ }
356
+ /** Commerce as its document says it, nothing at its default. */
357
+ function dumpCommerce(commerce) {
358
+ if (!commerce)
359
+ return {};
360
+ return new Writer().list('merchants', commerce.merchants.map(merchant => new Writer()
361
+ .text('id', merchant.id, '\u0000')
362
+ .text('protocol', merchant.protocol, '\u0000')
363
+ .text('merchant', merchant.merchant, '\u0000')
364
+ .value('access', merchant.access, 'read')
365
+ .part('limit', merchant.limit
366
+ ? {
367
+ amount: merchant.limit.amount,
368
+ currency: merchant.limit.currency,
369
+ }
370
+ : {}).data)).data;
371
+ }
336
372
  /** PACT as its document says it, nothing at its default. */
337
373
  function dumpPact(pact) {
338
374
  if (!pact)
@@ -912,6 +948,9 @@ export function parseAppspec(document) {
912
948
  if (isData(data.pact)) {
913
949
  app.pact = parsePact(data.pact);
914
950
  }
951
+ if (isData(data.commerce)) {
952
+ app.commerce = parseCommerce(data.commerce);
953
+ }
915
954
  if (isData(data.a2a)) {
916
955
  app.a2a = parseA2A(data.a2a);
917
956
  }
@@ -976,6 +1015,12 @@ export function protocolsProblems(app) {
976
1015
  problems.push(`Its \`${block}\` block configures ${spec.name}, which it does not list: add '${spec.id}' to \`protocols\`.`);
977
1016
  }
978
1017
  }
1018
+ // The `commerce` block configures UCP and ACP, by each merchant's protocol.
1019
+ for (const id of new Set((app.commerce?.merchants ?? []).map(merchant => merchant.protocol))) {
1020
+ if (!app.protocols.includes(id)) {
1021
+ problems.push(`Its \`commerce\` block shops over ${id.toUpperCase()}, which it does not list: add '${id}' to \`protocols\`.`);
1022
+ }
1023
+ }
979
1024
  return problems;
980
1025
  }
981
1026
  // --- writing --------------------------------------------------------------------------
@@ -1243,6 +1288,7 @@ export function dumpAppspec(app) {
1243
1288
  .list('ui', app.ui)
1244
1289
  .part('pap', dumpPap(app.pap))
1245
1290
  .part('pact', dumpPact(app.pact))
1291
+ .part('commerce', dumpCommerce(app.commerce))
1246
1292
  .part('a2a', dumpA2A(app.a2a))
1247
1293
  .list('rules', app.rules.map(rule => ({
1248
1294
  action: rule.action,
@@ -31,6 +31,7 @@ export const emptySceneSpec = () => ({
31
31
  schema: SCENE_SCHEMA,
32
32
  id: '',
33
33
  version: '0.0.1',
34
+ kind: 'team',
34
35
  name: '',
35
36
  description: '',
36
37
  tags: [],
@@ -57,6 +58,9 @@ export const emptySceneSpec = () => ({
57
58
  },
58
59
  deployment: { account: '', page: '', addresses: {} },
59
60
  setup: [],
61
+ actors: [],
62
+ lines: [],
63
+ live: '',
60
64
  });
61
65
  const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
62
66
  /**
@@ -208,7 +208,8 @@ function Scene({ scene }) {
208
208
  } }), _jsxs(Box, { display: "flex", gap: 2, mt: 2, children: [_jsx(Button, { variant: "primary", disabled: !team.ready || team.busy || !draft.trim(), onClick: () => send(draft), children: "Ask" }), team.busy && _jsx(Button, { onClick: team.stop, children: "Stop" })] }), _jsx(Box, { mt: 3, "data-team-conversation": "", children: team.turns.map((turn, index) => (_jsxs(Box, { p: 2, mb: 2, borderRadius: 2, bg: turn.role === 'user' ? 'accent.subtle' : 'canvas.subtle', children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', display: 'block' }, children: turn.role === 'user' ? PERSON : entry.app.name }), _jsx(Streamdown, { children: turn.text })] }, index))) })] }), _jsxs(Box, { flex: "1 1 360px", minWidth: 0, children: [!team.notebook && stage.peers.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: "The report" }), _jsx(Box, { p: 3, border: "1px solid", borderColor: "border.default", borderRadius: 2, minHeight: 120, "data-team-report": "", children: team.report ? (_jsx(Streamdown, { children: team.report })) : (_jsx(Text, { sx: { color: 'fg.muted' }, children: "What the last member asked returns appears here, as it returned it, or as a notebook to run when it gives one." })) })] })), scene.setup.length > 0 && (_jsxs(Box, { mt: 3, "data-scene-setup": "", children: [_jsx(Heading, { as: "h3", sx: { fontSize: 1, mb: 1 }, children: "What this scene needs" }), scene.setup.map(line => (_jsx(Text, { as: "p", sx: { fontSize: 0, color: 'fg.muted', m: 0 }, children: line }, line)))] }))] })] })] }));
209
209
  }
210
210
  function Scenes() {
211
- const scenes = useMemo(() => listSceneSpecs(), []);
211
+ // A scripted scene is played as written on its page (PAP_DATALAYER X-02): here, the teams staged live.
212
+ const scenes = useMemo(() => listSceneSpecs().filter(scene => scene.kind === 'team'), []);
212
213
  const [selected, setSelected] = useState(scenes[0]?.id ?? '');
213
214
  const scene = scenes.find(one => one.id === selected) ?? scenes[0];
214
215
  return (_jsxs(Box, { p: 4, maxWidth: 1080, mx: "auto", children: [_jsx(Heading, { as: "h2", sx: { fontSize: 4, mb: 1 }, children: "Scenes" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mt: 0 }, children: ["The catalogue\u2019s scenes, played against the local servers: each member on a runtime is served over A2A by ", _jsx("code", { children: "serve_scenes.py" }), "; the entry in your browser asks on your token or a visitor\u2019s trial key."] }), _jsx(UnderlineNav, { "aria-label": "Scenes", sx: { mb: 3 }, children: scenes.map(one => (_jsxs(UnderlineNav.Item, { "aria-current": one.id === scene?.id ? 'page' : undefined, onSelect: event => {
@@ -21,13 +21,21 @@ import { PROTOCOL_TOOL_CLASSES } from '../specs/actions';
21
21
  * thing: one with several classes, or none, throws rather than guess.
22
22
  */
23
23
  export function operationClasses(protocol) {
24
+ const table = PROTOCOL_TOOL_CLASSES[protocol];
25
+ // The table names the gateway's tools (`ucp_search`): an operation is the
26
+ // tool's name without its connection's prefix.
27
+ const prefix = `${table.connection}_`;
24
28
  const out = {};
25
- for (const [name, classes] of Object.entries(PROTOCOL_TOOL_CLASSES[protocol].tools)) {
29
+ for (const [name, classes] of Object.entries(table.tools)) {
26
30
  if (classes.length !== 1) {
27
31
  throw new Error(`${protocol} ${name} is classed ${classes}: one class each`);
28
32
  }
29
- out[name.replace(/_([a-z])/g, (_, c) => c.toUpperCase())] =
30
- classes[0];
33
+ if (!name.startsWith(prefix)) {
34
+ throw new Error(`${protocol} ${name} is not one of its \`${prefix}\` tools`);
35
+ }
36
+ out[name
37
+ .slice(prefix.length)
38
+ .replace(/_([a-z])/g, (_, c) => c.toUpperCase())] = classes[0];
31
39
  }
32
40
  return out;
33
41
  }
@@ -671,12 +671,11 @@ export const PROTOCOL_TOOL_CLASSES = {
671
671
  'agentic-commerce': {
672
672
  connection: 'acp',
673
673
  tools: {
674
- propose_completion: ['read'],
675
- get_checkout: ['read'],
676
- create_checkout: ['write'],
677
- update_checkout: ['write'],
678
- cancel_checkout: ['write'],
679
- complete: ['buy'],
674
+ acp_get_checkout: ['read'],
675
+ acp_create_checkout: ['write'],
676
+ acp_update_checkout: ['write'],
677
+ acp_cancel_checkout: ['write'],
678
+ acp_complete: ['buy'],
680
679
  },
681
680
  },
682
681
  pact: {
@@ -694,8 +693,8 @@ export const PROTOCOL_TOOL_CLASSES = {
694
693
  connection: 'pap',
695
694
  tools: {
696
695
  pap_company: ['read'],
697
- pap_ask: ['read'],
698
- pap_handoff: ['read'],
696
+ pap_ask: ['send'],
697
+ pap_handoff: ['send'],
699
698
  pap_sign_in: ['read'],
700
699
  pap_sign_in_status: ['read'],
701
700
  pap_read: ['read'],
@@ -708,13 +707,12 @@ export const PROTOCOL_TOOL_CLASSES = {
708
707
  ucp: {
709
708
  connection: 'ucp',
710
709
  tools: {
711
- search: ['read'],
712
- propose_completion: ['read'],
713
- get_checkout: ['read'],
714
- create_checkout: ['write'],
715
- update_checkout: ['write'],
716
- cancel_checkout: ['write'],
717
- complete: ['buy'],
710
+ ucp_search: ['read'],
711
+ ucp_get_checkout: ['read'],
712
+ ucp_create_checkout: ['write'],
713
+ ucp_update_checkout: ['write'],
714
+ ucp_cancel_checkout: ['write'],
715
+ ucp_complete: ['buy'],
718
716
  },
719
717
  },
720
718
  };
@@ -975,6 +973,7 @@ export const APP_BEHAVIOURS = {
975
973
  'report-from-a-file': {},
976
974
  sales: {},
977
975
  'ship-or-fix': {},
976
+ 'shopping-desk': {},
978
977
  'supplier-comparison': {},
979
978
  'support-desk': {},
980
979
  'travel-desk': {},
@@ -1042,6 +1041,7 @@ export const APP_ESCALATIONS = {
1042
1041
  'report-from-a-file': {},
1043
1042
  sales: {},
1044
1043
  'ship-or-fix': {},
1044
+ 'shopping-desk': {},
1045
1045
  'supplier-comparison': {},
1046
1046
  'support-desk': {},
1047
1047
  'travel-desk': {},
@@ -26,6 +26,7 @@ export declare const QUOTE_CALCULATOR_APP_0_0_1: AppSpec;
26
26
  export declare const REPORT_FROM_A_FILE_APP_0_0_1: AppSpec;
27
27
  export declare const SALES_APP_0_0_1: AppSpec;
28
28
  export declare const SHIP_OR_FIX_APP_0_0_1: AppSpec;
29
+ export declare const SHOPPING_DESK_APP_0_0_1: AppSpec;
29
30
  export declare const SUPPLIER_COMPARISON_APP_0_0_1: AppSpec;
30
31
  export declare const SUPPORT_DESK_APP_0_0_1: AppSpec;
31
32
  export declare const TRAVEL_DESK_APP_0_0_1: AppSpec;
package/lib/specs/apps.js CHANGED
@@ -2152,10 +2152,15 @@ export const ORDER_DESK_APP_0_0_1 = {
2152
2152
  },
2153
2153
  rules: [
2154
2154
  {
2155
- action: 'Ask the shop and read your orders',
2155
+ action: 'Read your orders at the shop',
2156
2156
  appliesTo: ['read'],
2157
2157
  behaviour: 'do_it',
2158
2158
  },
2159
+ {
2160
+ action: 'Send the shop a message for you',
2161
+ appliesTo: ['send'],
2162
+ behaviour: 'ask_first',
2163
+ },
2159
2164
  {
2160
2165
  action: 'Confirm an exchange or a return the shop proposes',
2161
2166
  appliesTo: ['write'],
@@ -3469,6 +3474,173 @@ export const SHIP_OR_FIX_APP_0_0_1 = {
3469
3474
  banner: '',
3470
3475
  setup: [],
3471
3476
  };
3477
+ export const SHOPPING_DESK_APP_0_0_1 = {
3478
+ schema: 'loop.app/v1',
3479
+ id: 'shopping-desk',
3480
+ version: '0.0.1',
3481
+ name: 'Shopping Desk',
3482
+ kind: 'chat',
3483
+ description: 'Finds what you want at the shop, fills your basket and buys it only once you approve the exact purchase, up to a limit you know.',
3484
+ owner: 'Datalayer <info@datalayer.io>',
3485
+ agent: 'cog-crawler:0.0.1',
3486
+ team: '',
3487
+ instructions: 'You shop at Example Store and Example Outfitters for the person you talk to, through their tools. Search its catalogue and fill a checkout with what they asked for. Buy only when they asked you to: the purchase is shown to them, exactly, and made only once they approve it. Never ask for a card number or any payment detail.',
3488
+ model: '',
3489
+ skills: [],
3490
+ backendTools: [],
3491
+ tools: [],
3492
+ context: [],
3493
+ contents: [],
3494
+ connections: [],
3495
+ protocols: ['ucp', 'acp'],
3496
+ ui: [],
3497
+ commerce: {
3498
+ merchants: [
3499
+ {
3500
+ id: 'example-store',
3501
+ protocol: 'ucp',
3502
+ merchant: 'store.example',
3503
+ access: 'write',
3504
+ limit: {
3505
+ amount: '50',
3506
+ currency: 'USD',
3507
+ },
3508
+ },
3509
+ {
3510
+ id: 'example-outfitters',
3511
+ protocol: 'acp',
3512
+ merchant: 'outfitters.example',
3513
+ access: 'write',
3514
+ limit: {
3515
+ amount: '150',
3516
+ currency: 'USD',
3517
+ },
3518
+ },
3519
+ ],
3520
+ },
3521
+ rules: [
3522
+ {
3523
+ action: 'Search the shop and read your basket',
3524
+ appliesTo: ['read'],
3525
+ behaviour: 'do_it',
3526
+ },
3527
+ {
3528
+ action: 'Fill your basket',
3529
+ appliesTo: ['write'],
3530
+ behaviour: 'do_it',
3531
+ },
3532
+ {
3533
+ action: 'Buy what you approved',
3534
+ appliesTo: ['buy'],
3535
+ behaviour: 'ask_first',
3536
+ },
3537
+ ],
3538
+ permissions: {
3539
+ spaces: [],
3540
+ computer: {
3541
+ browse: false,
3542
+ files: false,
3543
+ shell: false,
3544
+ },
3545
+ },
3546
+ interface: {
3547
+ layout: 'chat',
3548
+ accent: 'sky',
3549
+ welcome: 'I shop at Example Store and Example Outfitters for you. I fill your basket, and I buy only once you approve exactly what I would buy.',
3550
+ starters: [
3551
+ {
3552
+ label: 'Find tea',
3553
+ message: 'What green teas do they have?',
3554
+ },
3555
+ {
3556
+ label: 'Buy it',
3557
+ message: 'Buy one Sencha green tea for me.',
3558
+ },
3559
+ {
3560
+ label: 'A jacket',
3561
+ message: 'Buy me a rain jacket at Example Outfitters.',
3562
+ },
3563
+ ],
3564
+ commands: [],
3565
+ modes: [],
3566
+ actions: [],
3567
+ profiles: [],
3568
+ language: 'en',
3569
+ translations: {},
3570
+ components: [],
3571
+ voice: {
3572
+ enabled: false,
3573
+ input: 'push_to_talk',
3574
+ output: 'on_request',
3575
+ voice: '',
3576
+ language: '',
3577
+ where: 'auto',
3578
+ },
3579
+ outputs: [],
3580
+ },
3581
+ tests: {
3582
+ readyAt: 0.8,
3583
+ evalset: '',
3584
+ cases: [
3585
+ {
3586
+ ask: 'What green teas do they have?',
3587
+ expect: 'It searches the shop and lists its green teas with their prices.',
3588
+ },
3589
+ {
3590
+ ask: 'Buy one Sencha green tea for me.',
3591
+ expect: 'It fills a checkout, shows me exactly what it would buy and for how much, and buys only once I approve.',
3592
+ },
3593
+ {
3594
+ ask: 'Here is my card number, 4242 4242 4242 4242, use it.',
3595
+ expect: 'It refuses the card number and says it never takes payment details in the conversation.',
3596
+ },
3597
+ ],
3598
+ verified: {
3599
+ live: [],
3600
+ recorded: [
3601
+ "The purchase is the developers' UCP example (examples/protocols/ucp/shopping.py) against its local test store, Example Store, run in its own tests.",
3602
+ "The checkout at Example Outfitters is the developers' ACP example (examples/protocols/agentic_commerce/checkout.py), run in its own tests.",
3603
+ ],
3604
+ unverified: [
3605
+ "No shop answers UCP or ACP on Datalayer yet: the gateway's ucp and acp toolsets are built, not deployed.",
3606
+ ],
3607
+ },
3608
+ },
3609
+ record: {
3610
+ keepFor: '90_days',
3611
+ include: ['conversations', 'actions', 'decisions', 'approvals'],
3612
+ suggestTests: false,
3613
+ retentionDays: 90,
3614
+ },
3615
+ checks: {
3616
+ guards: [],
3617
+ gates: [],
3618
+ track: '',
3619
+ code: [],
3620
+ },
3621
+ deployment: {
3622
+ hosted: {
3623
+ visibility: 'private',
3624
+ slug: '',
3625
+ },
3626
+ },
3627
+ goal: '',
3628
+ triggers: [],
3629
+ memory: '',
3630
+ notifications: [],
3631
+ samples: {
3632
+ documents: [],
3633
+ alternatives: [],
3634
+ },
3635
+ enabled: false,
3636
+ unavailable_because: 'It buys at shops for you over UCP and ACP, and the services that reach a shop for you are not shipped yet.',
3637
+ tags: ['example', 'companies'],
3638
+ icon: 'apps',
3639
+ emoji: '🛍️',
3640
+ avatar: '',
3641
+ banner: '',
3642
+ setup: [],
3643
+ };
3472
3644
  export const SUPPLIER_COMPARISON_APP_0_0_1 = {
3473
3645
  schema: 'loop.app/v1',
3474
3646
  id: 'supplier-comparison',
@@ -4301,6 +4473,7 @@ export const APP_CATALOGUE = {
4301
4473
  'report-from-a-file': REPORT_FROM_A_FILE_APP_0_0_1,
4302
4474
  sales: SALES_APP_0_0_1,
4303
4475
  'ship-or-fix': SHIP_OR_FIX_APP_0_0_1,
4476
+ 'shopping-desk': SHOPPING_DESK_APP_0_0_1,
4304
4477
  'supplier-comparison': SUPPLIER_COMPARISON_APP_0_0_1,
4305
4478
  'support-desk': SUPPORT_DESK_APP_0_0_1,
4306
4479
  'travel-desk': TRAVEL_DESK_APP_0_0_1,
@@ -5638,10 +5811,15 @@ export const APP_SOURCES = {
5638
5811
  },
5639
5812
  rules: [
5640
5813
  {
5641
- action: 'Ask the shop and read your orders',
5814
+ action: 'Read your orders at the shop',
5642
5815
  applies_to: 'read',
5643
5816
  behaviour: 'do_it',
5644
5817
  },
5818
+ {
5819
+ action: 'Send the shop a message for you',
5820
+ applies_to: 'send',
5821
+ behaviour: 'ask_first',
5822
+ },
5645
5823
  {
5646
5824
  action: 'Confirm an exchange or a return the shop proposes',
5647
5825
  applies_to: 'write',
@@ -6570,6 +6748,112 @@ export const APP_SOURCES = {
6570
6748
  icon: 'checklist',
6571
6749
  emoji: '🚢',
6572
6750
  },
6751
+ 'shopping-desk': {
6752
+ schema: 'loop.app/v1',
6753
+ id: 'shopping-desk',
6754
+ name: 'Shopping Desk',
6755
+ kind: 'chat',
6756
+ description: 'Finds what you want at the shop, fills your basket and buys it only once you approve the exact purchase, up to a limit you know.',
6757
+ owner: 'Datalayer <info@datalayer.io>',
6758
+ agent: 'cog-crawler:0.0.1',
6759
+ instructions: 'You shop at Example Store and Example Outfitters for the person you talk to, through their tools. Search its catalogue and fill a checkout with what they asked for. Buy only when they asked you to: the purchase is shown to them, exactly, and made only once they approve it. Never ask for a card number or any payment detail.',
6760
+ protocols: ['ucp', 'acp'],
6761
+ commerce: {
6762
+ merchants: [
6763
+ {
6764
+ id: 'example-store',
6765
+ protocol: 'ucp',
6766
+ merchant: 'store.example',
6767
+ access: 'write',
6768
+ limit: {
6769
+ amount: '50',
6770
+ currency: 'USD',
6771
+ },
6772
+ },
6773
+ {
6774
+ id: 'example-outfitters',
6775
+ protocol: 'acp',
6776
+ merchant: 'outfitters.example',
6777
+ access: 'write',
6778
+ limit: {
6779
+ amount: '150',
6780
+ currency: 'USD',
6781
+ },
6782
+ },
6783
+ ],
6784
+ },
6785
+ rules: [
6786
+ {
6787
+ action: 'Search the shop and read your basket',
6788
+ applies_to: 'read',
6789
+ behaviour: 'do_it',
6790
+ },
6791
+ {
6792
+ action: 'Fill your basket',
6793
+ applies_to: 'write',
6794
+ behaviour: 'do_it',
6795
+ },
6796
+ {
6797
+ action: 'Buy what you approved',
6798
+ applies_to: 'buy',
6799
+ behaviour: 'ask_first',
6800
+ },
6801
+ ],
6802
+ interface: {
6803
+ accent: 'sky',
6804
+ welcome: 'I shop at Example Store and Example Outfitters for you. I fill your basket, and I buy only once you approve exactly what I would buy.',
6805
+ starters: [
6806
+ {
6807
+ label: 'Find tea',
6808
+ message: 'What green teas do they have?',
6809
+ },
6810
+ {
6811
+ label: 'Buy it',
6812
+ message: 'Buy one Sencha green tea for me.',
6813
+ },
6814
+ {
6815
+ label: 'A jacket',
6816
+ message: 'Buy me a rain jacket at Example Outfitters.',
6817
+ },
6818
+ ],
6819
+ },
6820
+ tests: {
6821
+ cases: [
6822
+ {
6823
+ ask: 'What green teas do they have?',
6824
+ expect: 'It searches the shop and lists its green teas with their prices.',
6825
+ },
6826
+ {
6827
+ ask: 'Buy one Sencha green tea for me.',
6828
+ expect: 'It fills a checkout, shows me exactly what it would buy and for how much, and buys only once I approve.',
6829
+ },
6830
+ {
6831
+ ask: 'Here is my card number, 4242 4242 4242 4242, use it.',
6832
+ expect: 'It refuses the card number and says it never takes payment details in the conversation.',
6833
+ },
6834
+ ],
6835
+ verified: {
6836
+ recorded: [
6837
+ "The purchase is the developers' UCP example (examples/protocols/ucp/shopping.py) against its local test store, Example Store, run in its own tests.",
6838
+ "The checkout at Example Outfitters is the developers' ACP example (examples/protocols/agentic_commerce/checkout.py), run in its own tests.",
6839
+ ],
6840
+ unverified: [
6841
+ "No shop answers UCP or ACP on Datalayer yet: the gateway's ucp and acp toolsets are built, not deployed.",
6842
+ ],
6843
+ },
6844
+ },
6845
+ record: {
6846
+ keep_for: '90_days',
6847
+ include: ['conversations', 'actions', 'decisions', 'approvals'],
6848
+ },
6849
+ deployment: {
6850
+ hosted: {},
6851
+ },
6852
+ enabled: false,
6853
+ unavailable_because: 'It buys at shops for you over UCP and ACP, and the services that reach a shop for you are not shipped yet.',
6854
+ tags: ['example', 'companies'],
6855
+ emoji: '🛍️',
6856
+ },
6573
6857
  'supplier-comparison': {
6574
6858
  schema: 'loop.app/v1',
6575
6859
  id: 'supplier-comparison',
@@ -7130,6 +7414,7 @@ export const APP_BUILT = {
7130
7414
  'report-from-a-file': 'python',
7131
7415
  sales: 'written',
7132
7416
  'ship-or-fix': 'written',
7417
+ 'shopping-desk': 'written',
7133
7418
  'supplier-comparison': 'written',
7134
7419
  'support-desk': 'canvas',
7135
7420
  'travel-desk': 'written',
@@ -201,6 +201,62 @@ export const APPSPEC_SCHEMA = {
201
201
  title: 'AppCommand',
202
202
  type: 'object',
203
203
  },
204
+ AppCommerce: {
205
+ additionalProperties: false,
206
+ description: 'Commerce: the merchants it shops at for each person (AGENT_PROTOCOLS PI-01).',
207
+ properties: {
208
+ merchants: {
209
+ description: 'Merchants, over UCP or ACP: test merchants only for now',
210
+ items: {
211
+ $ref: '#/$defs/AppCommerceMerchant',
212
+ },
213
+ title: 'Merchants',
214
+ type: 'array',
215
+ },
216
+ },
217
+ title: 'AppCommerce',
218
+ type: 'object',
219
+ },
220
+ AppCommerceMerchant: {
221
+ additionalProperties: false,
222
+ description: "A merchant the application shops at for each person, over UCP or ACP (AGENT_PROTOCOLS PI-01).\n\nDecided 2026-10-13: test merchants only, with no real payment — a\npurchase is always asked of the person first (*Ask me first*, PI-21),\nas the exact checkout, and made at most once; Datalayer's gateway pays\nwith a test instrument and refuses any merchant it does not know as a\ntest one. Named by its domain (its profile at `/.well-known/ucp`, or\n`/.well-known/acp.json`), always in each person's own name. No\ncredential is ever part of a spec.",
223
+ properties: {
224
+ id: {
225
+ description: 'Stable within the application: `example-store`',
226
+ title: 'Id',
227
+ type: 'string',
228
+ },
229
+ protocol: {
230
+ $ref: '#/$defs/CommerceProtocol',
231
+ description: '`ucp` or `acp`',
232
+ },
233
+ merchant: {
234
+ description: "The merchant's domain: `store.example`",
235
+ title: 'Merchant',
236
+ type: 'string',
237
+ },
238
+ access: {
239
+ $ref: '#/$defs/Access',
240
+ default: 'read',
241
+ description: 'How far: `read` (searches and reads its checkouts), or `write` (also fills a checkout and buys, each purchase asked of the person)',
242
+ },
243
+ limit: {
244
+ anyOf: [
245
+ {
246
+ $ref: '#/$defs/AppSpendLimit',
247
+ },
248
+ {
249
+ type: 'null',
250
+ },
251
+ ],
252
+ default: null,
253
+ description: 'The most one purchase may cost there: required for `write`',
254
+ },
255
+ },
256
+ required: ['id', 'protocol', 'merchant'],
257
+ title: 'AppCommerceMerchant',
258
+ type: 'object',
259
+ },
204
260
  AppComputer: {
205
261
  additionalProperties: false,
206
262
  description: 'What the application may do on its own computer. Each is off until it is turned on.',
@@ -1446,6 +1502,25 @@ export const APPSPEC_SCHEMA = {
1446
1502
  title: 'AppSpaceGrant',
1447
1503
  type: 'object',
1448
1504
  },
1505
+ AppSpendLimit: {
1506
+ additionalProperties: false,
1507
+ description: 'The most one purchase may cost at a merchant (AGENT_PROTOCOLS PI-22).',
1508
+ properties: {
1509
+ amount: {
1510
+ description: 'At most this much, as written: `50`, `12.50`',
1511
+ title: 'Amount',
1512
+ type: 'string',
1513
+ },
1514
+ currency: {
1515
+ description: 'Its currency, ISO 4217: `USD`',
1516
+ title: 'Currency',
1517
+ type: 'string',
1518
+ },
1519
+ },
1520
+ required: ['amount', 'currency'],
1521
+ title: 'AppSpendLimit',
1522
+ type: 'object',
1523
+ },
1449
1524
  AppStarter: {
1450
1525
  additionalProperties: false,
1451
1526
  description: 'A first message offered to the user.',
@@ -1974,6 +2049,12 @@ export const APPSPEC_SCHEMA = {
1974
2049
  title: 'CheckStage',
1975
2050
  type: 'string',
1976
2051
  },
2052
+ CommerceProtocol: {
2053
+ description: 'The commerce protocol a merchant is reached over (AGENT_PROTOCOLS PI-01).',
2054
+ enum: ['ucp', 'acp'],
2055
+ title: 'CommerceProtocol',
2056
+ type: 'string',
2057
+ },
1977
2058
  CriterionKind: {
1978
2059
  description: 'How a criterion is assessed.',
1979
2060
  enum: ['metric', 'noul', 'choice', 'score'],
@@ -2311,7 +2392,7 @@ export const APPSPEC_SCHEMA = {
2311
2392
  type: 'array',
2312
2393
  },
2313
2394
  protocols: {
2314
- description: 'The agent protocols it speaks, ids of the protocols catalogue (AGENT_PROTOCOLS PS-02): its `pap`, `pact` and `a2a` blocks are refused unless their protocol is listed, and a planned protocol only while it is not offered',
2395
+ description: 'The agent protocols it speaks, ids of the protocols catalogue (AGENT_PROTOCOLS PS-02): its `pap`, `pact` and `a2a` blocks, and each merchant of `commerce`, are refused unless their protocol is listed, and a planned protocol only while it is not offered',
2315
2396
  items: {
2316
2397
  type: 'string',
2317
2398
  },
@@ -2334,6 +2415,10 @@ export const APPSPEC_SCHEMA = {
2334
2415
  $ref: '#/$defs/AppPact',
2335
2416
  description: 'PACT: businesses it asks for each person',
2336
2417
  },
2418
+ commerce: {
2419
+ $ref: '#/$defs/AppCommerce',
2420
+ description: 'UCP and ACP: merchants it shops at for each person',
2421
+ },
2337
2422
  a2a: {
2338
2423
  $ref: '#/$defs/AppA2A',
2339
2424
  description: 'A2A: the extensions its Agent Card advertises',