@datalayer/agent-runtimes 1.3.106 → 1.3.109

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.
package/lib/specs/apps.js CHANGED
@@ -558,20 +558,78 @@ export const CUSTOMER_INTERVIEW_APP_0_0_1 = {
558
558
  evalset: '',
559
559
  cases: [
560
560
  {
561
- ask: 'The interviewee declines to be recorded.',
562
- expect: 'It thanks them, asks nothing more, and saves no insight.',
561
+ ask: '',
562
+ turns: [
563
+ {
564
+ choose: 'No',
565
+ },
566
+ {
567
+ say: 'Ask me why I stopped after the trial.',
568
+ },
569
+ ],
570
+ expect: 'It thanks them, asks nothing more, even when they write again, and saves no insight.',
563
571
  },
564
572
  {
565
- ask: 'We want to learn why people leave after the trial.',
566
- expect: 'It asks open questions about the trial, one at a time, and none that suggests an answer.',
573
+ ask: '',
574
+ turns: [
575
+ {
576
+ choose: 'Yes',
577
+ },
578
+ {
579
+ say: 'We want to learn why people leave after the trial.',
580
+ },
581
+ {
582
+ say: 'I stopped using it when the trial ended.',
583
+ },
584
+ ],
585
+ expect: 'It asks one open question at a time about the trial, and none that suggests an answer.',
567
586
  },
568
587
  {
569
- ask: 'The interviewee says the price was fine but the setup took a week.',
570
- expect: 'It follows up on the setup, and the insight it saves quotes their words about it.',
588
+ ask: '',
589
+ turns: [
590
+ {
591
+ choose: 'Yes',
592
+ },
593
+ {
594
+ say: 'We want to learn why people leave after the trial.',
595
+ },
596
+ {
597
+ say: 'The price was fine, but the setup took a week.',
598
+ },
599
+ {
600
+ press: 'save',
601
+ payload: {
602
+ insight: 'The setup, not the price, held them back.',
603
+ quote: 'The price was fine, but the setup took a week.',
604
+ },
605
+ },
606
+ ],
607
+ expect: 'It follows up on the setup, and the insight saved quotes their words about it.',
571
608
  },
572
609
  {
573
- ask: 'End the interview.',
574
- expect: 'It gives the goal, the insights each with its quote, and the questions left open.',
610
+ ask: '',
611
+ turns: [
612
+ {
613
+ choose: 'Yes',
614
+ },
615
+ {
616
+ say: 'We want to learn why people leave after the trial.',
617
+ },
618
+ {
619
+ say: 'The price was fine, but the setup took a week.',
620
+ },
621
+ {
622
+ press: 'save',
623
+ payload: {
624
+ insight: 'The setup, not the price, held them back.',
625
+ quote: 'The price was fine, but the setup took a week.',
626
+ },
627
+ },
628
+ {
629
+ press: 'finish',
630
+ },
631
+ ],
632
+ expect: 'It records the result: the goal, the insight saved with its quote, and the questions the interview left open, which it lists.',
575
633
  },
576
634
  ],
577
635
  verified: {
@@ -2139,7 +2197,7 @@ export const QUOTE_CALCULATOR_APP_0_0_1 = {
2139
2197
  owner: 'Datalayer <info@datalayer.io>',
2140
2198
  agent: 'jupyter-data-analyst:0.0.1',
2141
2199
  team: '',
2142
- instructions: 'Compute the quote in code from the inputs and the price list. Show each line of the calculation; never estimate a total.',
2200
+ instructions: "Before you write a word, search the price list with search_documents, for every request, even one you refuse. Then compute the quote in code from the inputs and the passages it found. Show each line of the calculation; never estimate a total. A quote needs at least one seat, and at least the plan's minimum_seats of the price list: for fewer, refuse in a sentence that says so, compute nothing, and do not ask what was meant. Call a tool only to read the price list or to compute, never to say or announce something.",
2143
2201
  model: '',
2144
2202
  skills: [],
2145
2203
  backendTools: [],
@@ -2409,7 +2467,7 @@ export const REPORT_FROM_A_FILE_APP_0_0_1 = {
2409
2467
  owner: 'Datalayer <info@datalayer.io>',
2410
2468
  agent: 'jupyter-data-analyst:0.0.1',
2411
2469
  team: '',
2412
- instructions: 'Analyse the file in code, in the sandbox. Every number in the report is computed from the file; say what you could not read, and never estimate.',
2470
+ instructions: 'You take a CSV file only: asked for a file of another kind (a PDF, a spreadsheet, an image), refuse it in a sentence that says you take a CSV, and ask for nothing. Analyse the file in code, in the sandbox. Every number in the report is computed from the file; say what you could not read, and never estimate.',
2413
2471
  model: '',
2414
2472
  skills: [],
2415
2473
  backendTools: [],
@@ -3233,7 +3291,7 @@ export const SUPPORT_DESK_APP_0_0_1 = {
3233
3291
  owner: 'Datalayer <info@datalayer.io>',
3234
3292
  agent: 'worker-document-qa:0.0.1',
3235
3293
  team: '',
3236
- instructions: "Answer from the documents you were given only, and cite the passage each answer rests on. When they do not hold the answer, say so and offer to hand the question to a person; never guess. Do nothing on an account: changing, refunding or deleting is a person's.",
3294
+ instructions: "Before you write a word, search the documents you were given, for every message, even a request you decline; answer from them only, and cite the passage each answer rests on. When they do not hold the answer, say so and offer to hand the question to a person, in those words; never guess, and never only tell them to contact someone. Do nothing on an account: changing, refunding or deleting is a person's. Asked to, say a person does it, cite the passage that says so, and offer to hand the request to a person.",
3237
3295
  model: '',
3238
3296
  skills: [],
3239
3297
  backendTools: [],
@@ -4054,20 +4112,74 @@ export const APP_SOURCES = {
4054
4112
  tests: {
4055
4113
  cases: [
4056
4114
  {
4057
- ask: 'The interviewee declines to be recorded.',
4058
- expect: 'It thanks them, asks nothing more, and saves no insight.',
4115
+ turns: [
4116
+ {
4117
+ choose: 'No',
4118
+ },
4119
+ {
4120
+ say: 'Ask me why I stopped after the trial.',
4121
+ },
4122
+ ],
4123
+ expect: 'It thanks them, asks nothing more, even when they write again, and saves no insight.',
4059
4124
  },
4060
4125
  {
4061
- ask: 'We want to learn why people leave after the trial.',
4062
- expect: 'It asks open questions about the trial, one at a time, and none that suggests an answer.',
4126
+ turns: [
4127
+ {
4128
+ choose: 'Yes',
4129
+ },
4130
+ {
4131
+ say: 'We want to learn why people leave after the trial.',
4132
+ },
4133
+ {
4134
+ say: 'I stopped using it when the trial ended.',
4135
+ },
4136
+ ],
4137
+ expect: 'It asks one open question at a time about the trial, and none that suggests an answer.',
4063
4138
  },
4064
4139
  {
4065
- ask: 'The interviewee says the price was fine but the setup took a week.',
4066
- expect: 'It follows up on the setup, and the insight it saves quotes their words about it.',
4140
+ turns: [
4141
+ {
4142
+ choose: 'Yes',
4143
+ },
4144
+ {
4145
+ say: 'We want to learn why people leave after the trial.',
4146
+ },
4147
+ {
4148
+ say: 'The price was fine, but the setup took a week.',
4149
+ },
4150
+ {
4151
+ press: 'save',
4152
+ payload: {
4153
+ insight: 'The setup, not the price, held them back.',
4154
+ quote: 'The price was fine, but the setup took a week.',
4155
+ },
4156
+ },
4157
+ ],
4158
+ expect: 'It follows up on the setup, and the insight saved quotes their words about it.',
4067
4159
  },
4068
4160
  {
4069
- ask: 'End the interview.',
4070
- expect: 'It gives the goal, the insights each with its quote, and the questions left open.',
4161
+ turns: [
4162
+ {
4163
+ choose: 'Yes',
4164
+ },
4165
+ {
4166
+ say: 'We want to learn why people leave after the trial.',
4167
+ },
4168
+ {
4169
+ say: 'The price was fine, but the setup took a week.',
4170
+ },
4171
+ {
4172
+ press: 'save',
4173
+ payload: {
4174
+ insight: 'The setup, not the price, held them back.',
4175
+ quote: 'The price was fine, but the setup took a week.',
4176
+ },
4177
+ },
4178
+ {
4179
+ press: 'finish',
4180
+ },
4181
+ ],
4182
+ expect: 'It records the result: the goal, the insight saved with its quote, and the questions the interview left open, which it lists.',
4071
4183
  },
4072
4184
  ],
4073
4185
  verified: {
@@ -5094,7 +5206,7 @@ export const APP_SOURCES = {
5094
5206
  description: 'Computes a quote from a number of seats, a plan and a term, and shows how the total is made.',
5095
5207
  owner: 'Datalayer <info@datalayer.io>',
5096
5208
  agent: 'jupyter-data-analyst:0.0.1',
5097
- instructions: 'Compute the quote in code from the inputs and the price list. Show each line of the calculation; never estimate a total.',
5209
+ instructions: "Before you write a word, search the price list with search_documents, for every request, even one you refuse. Then compute the quote in code from the inputs and the passages it found. Show each line of the calculation; never estimate a total. A quote needs at least one seat, and at least the plan's minimum_seats of the price list: for fewer, refuse in a sentence that says so, compute nothing, and do not ask what was meant. Call a tool only to read the price list or to compute, never to say or announce something.",
5098
5210
  contents: ['Price list'],
5099
5211
  interface: {
5100
5212
  accent: 'sun',
@@ -5302,7 +5414,7 @@ export const APP_SOURCES = {
5302
5414
  description: 'Takes a CSV file, analyses it in the sandbox, and gives back a report: what the data holds, what stands out, and what is missing.',
5303
5415
  owner: 'Datalayer <info@datalayer.io>',
5304
5416
  agent: 'jupyter-data-analyst:0.0.1',
5305
- instructions: 'Analyse the file in code, in the sandbox. Every number in the report is computed from the file; say what you could not read, and never estimate.',
5417
+ instructions: 'You take a CSV file only: asked for a file of another kind (a PDF, a spreadsheet, an image), refuse it in a sentence that says you take a CSV, and ask for nothing. Analyse the file in code, in the sandbox. Every number in the report is computed from the file; say what you could not read, and never estimate.',
5306
5418
  rules: [
5307
5419
  {
5308
5420
  action: 'Send the report by email',
@@ -5841,7 +5953,7 @@ export const APP_SOURCES = {
5841
5953
  description: 'Answers product questions from the documentation it was given, cites the passage, and says when the documentation does not hold the answer.',
5842
5954
  owner: 'Datalayer <info@datalayer.io>',
5843
5955
  agent: 'worker-document-qa:0.0.1',
5844
- instructions: "Answer from the documents you were given only, and cite the passage each answer rests on. When they do not hold the answer, say so and offer to hand the question to a person; never guess. Do nothing on an account: changing, refunding or deleting is a person's.",
5956
+ instructions: "Before you write a word, search the documents you were given, for every message, even a request you decline; answer from them only, and cite the passage each answer rests on. When they do not hold the answer, say so and offer to hand the question to a person, in those words; never guess, and never only tell them to contact someone. Do nothing on an account: changing, refunding or deleting is a person's. Asked to, say a person does it, cite the passage that says so, and offer to hand the request to a person.",
5845
5957
  contents: ['Product documentation', 'Returns policy'],
5846
5958
  interface: {
5847
5959
  layout: 'page',
@@ -1208,13 +1208,22 @@ export const APPSPEC_SCHEMA = {
1208
1208
  },
1209
1209
  AppTestCase: {
1210
1210
  additionalProperties: false,
1211
- description: "An example of what the application should do, in plain words.\n\nWhen plain words are not enough, its code decides it (LOOP P-06): `code`\nnames the function of the application's `app.py` that is given the\nconversation and says whether it passed — `@app.test` writes it. `expect`\nstill says it in words: without its file, the case is judged by them.",
1211
+ description: "An example of what the application should do, in plain words.\n\nA case is one message (`ask`) or a short conversation (`turns`): what\nthe person says, chooses and presses, in order, each after the\napplication answered the turn before — and what it should do is judged\non the whole conversation.\n\nWhen plain words are not enough, its code decides it (LOOP P-06): `code`\nnames the function of the application's `app.py` that is given the\nconversation and says whether it passed — `@app.test` writes it. `expect`\nstill says it in words: without its file, the case is judged by them.",
1212
1212
  properties: {
1213
1213
  ask: {
1214
- description: 'What it is asked',
1214
+ default: '',
1215
+ description: 'What it is asked, in one message; none when it has turns',
1215
1216
  title: 'Ask',
1216
1217
  type: 'string',
1217
1218
  },
1219
+ turns: {
1220
+ description: 'The conversation it is had in, turn by turn, in place of one message: what the person says, chooses and presses',
1221
+ items: {
1222
+ $ref: '#/$defs/AppTestTurn',
1223
+ },
1224
+ title: 'Turns',
1225
+ type: 'array',
1226
+ },
1218
1227
  expect: {
1219
1228
  description: 'What it should do',
1220
1229
  title: 'Expect',
@@ -1236,7 +1245,7 @@ export const APPSPEC_SCHEMA = {
1236
1245
  type: 'array',
1237
1246
  },
1238
1247
  },
1239
- required: ['ask', 'expect'],
1248
+ required: ['expect'],
1240
1249
  title: 'AppTestCase',
1241
1250
  type: 'object',
1242
1251
  },
@@ -1259,6 +1268,39 @@ export const APPSPEC_SCHEMA = {
1259
1268
  title: 'AppTestFile',
1260
1269
  type: 'object',
1261
1270
  },
1271
+ AppTestTurn: {
1272
+ additionalProperties: false,
1273
+ description: "One turn of a test's conversation: what the person says, the option of a\nchoice they pick, or an action of its code they press, with its payload.\n\nEach is sent as its page sends it, after the application answered the turn\nbefore: a message, a choice answered in its option's words, an action by\nits name (``@app.action``).",
1274
+ properties: {
1275
+ say: {
1276
+ default: '',
1277
+ description: 'What the person says',
1278
+ title: 'Say',
1279
+ type: 'string',
1280
+ },
1281
+ choose: {
1282
+ default: '',
1283
+ description: 'The option they pick, of the choice it asked',
1284
+ title: 'Choose',
1285
+ type: 'string',
1286
+ },
1287
+ press: {
1288
+ default: '',
1289
+ description: 'The action of its code they press, by name (`@app.action`)',
1290
+ pattern: '^(?:[A-Za-z_][A-Za-z0-9_]*)?$',
1291
+ title: 'Press',
1292
+ type: 'string',
1293
+ },
1294
+ payload: {
1295
+ additionalProperties: true,
1296
+ description: 'What the action is given with its press',
1297
+ title: 'Payload',
1298
+ type: 'object',
1299
+ },
1300
+ },
1301
+ title: 'AppTestTurn',
1302
+ type: 'object',
1303
+ },
1262
1304
  AppTests: {
1263
1305
  additionalProperties: false,
1264
1306
  description: 'How the application is verified.',
@@ -372,6 +372,8 @@ export interface ServerActionsSpec {
372
372
  tools: Record<string, ActionClass[]>;
373
373
  /** What an argument makes a tool do besides, by tool. */
374
374
  conditions: Record<string, ActionConditionSpec[]>;
375
+ /** The argument carrying what a tool sends, signed with the application's byline (LOOP I-10), by tool. */
376
+ signs?: Record<string, string>;
375
377
  }
376
378
  /** What kind of application it is: what its user meets. */
377
379
  export type AppKind = 'chat' | 'widget' | 'decision' | 'worker';
@@ -744,8 +746,29 @@ export interface AppTestFileSpec {
744
746
  /** What it holds. */
745
747
  text: string;
746
748
  }
749
+ /**
750
+ * One turn of a test's conversation: what the person says, the option of a
751
+ * choice they pick, or an action of its code they press with its payload —
752
+ * exactly one of `say`, `choose` and `press`.
753
+ */
754
+ export interface AppTestTurnSpec {
755
+ /** What the person says. */
756
+ say?: string;
757
+ /** The option they pick, of the choice it asked. */
758
+ choose?: string;
759
+ /** The action of its code they press, by name (`@app.action`). */
760
+ press?: string;
761
+ /** What the action is given with its press. */
762
+ payload?: Record<string, unknown>;
763
+ }
747
764
  export interface AppTestCaseSpec {
765
+ /** What it is asked, in one message; empty when it has `turns`. */
748
766
  ask: string;
767
+ /**
768
+ * The conversation it is had in, turn by turn, in place of one message,
769
+ * judged whole. Absent for a case of one message.
770
+ */
771
+ turns?: AppTestTurnSpec[];
749
772
  expect: string;
750
773
  /**
751
774
  * The function of its code that decides the case, by name (LOOP P-06:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalayer/agent-runtimes",
3
- "version": "1.3.106",
3
+ "version": "1.3.109",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  ".",
@@ -160,7 +160,7 @@
160
160
  "@datalayer/lexical-loro": "^1.0.6",
161
161
  "@datalayer/primer-addons": "^1.0.39",
162
162
  "@datalayer/primer-rjsf": "^1.0.3",
163
- "@datalayer/personal-agent-protocol": "^0.3.1",
163
+ "@datalayer/personal-agent-protocol": "^0.3.3",
164
164
  "@datalayer/reactor": "^1.0.4",
165
165
  "@datalayer/reactor-commands": "^1.0.4",
166
166
  "@datalayer/reactor-manager": "^1.0.4",
@@ -131,6 +131,15 @@ def load_specs(specs_dir: Path) -> tuple[list[dict[str, Any]], dict[str, Any]]:
131
131
  # A case given files names them; the others say nothing of them.
132
132
  if not case.get("files"):
133
133
  case.pop("files", None)
134
+ # A conversation says its turns, each by what it does (`say`,
135
+ # `choose`, or `press` with its payload); a case of one message
136
+ # says none, and its `ask` is empty when it has them.
137
+ if not case.get("turns"):
138
+ case.pop("turns", None)
139
+ for turn in case.get("turns", []):
140
+ for key in ("say", "choose", "press", "payload"):
141
+ if not turn.get(key):
142
+ turn.pop(key, None)
134
143
  # Components of its own: said only when it has some.
135
144
  if not spec["interface"].get("custom_components"):
136
145
  spec["interface"].pop("custom_components", None)
@@ -171,6 +180,12 @@ def load_specs(specs_dir: Path) -> tuple[list[dict[str, Any]], dict[str, Any]]:
171
180
  },
172
181
  # What an argument makes it do besides.
173
182
  "conditions": {name: found for name, found in conditions.items() if found},
183
+ # The argument carrying what it sends, signed with the byline (I-10).
184
+ "signs": {
185
+ name: signs
186
+ for name in names
187
+ if (signs := actions_module.server_tool_signs(server, name))
188
+ },
174
189
  }
175
190
  actions = {
176
191
  "classes": [item.value for item in actions_module.ActionClass],
@@ -12,6 +12,17 @@ from agent_runtimes.loop.apps.build import build
12
12
 
13
13
  ROOT = Path(__file__).parents[1]
14
14
  EXAMPLES = ROOT / "examples" / "personal-agent-protocol"
15
+ HOST_ONLY_TOOL_ARGUMENTS = {
16
+ "access_token",
17
+ "authorization_code",
18
+ "callback_url",
19
+ "client_assertion",
20
+ "code_verifier",
21
+ "local_user_id",
22
+ "pairwise_user_id",
23
+ "redirect_uri",
24
+ "session_token",
25
+ }
15
26
 
16
27
 
17
28
  def main() -> int:
@@ -33,6 +44,13 @@ def main() -> int:
33
44
  if key in tool_names:
34
45
  raise RuntimeError(f"Duplicate example tool: {app_id}/{tool.name}")
35
46
  tool_names.add(key)
47
+ properties = tool.parameters.get("properties", {})
48
+ unsafe = HOST_ONLY_TOOL_ARGUMENTS.intersection(properties)
49
+ if unsafe:
50
+ names = ", ".join(sorted(unsafe))
51
+ raise RuntimeError(
52
+ f"Host-only PAP values exposed by {app_id}/{tool.name}: {names}"
53
+ )
36
54
  sys.stdout.write(
37
55
  f"{path.relative_to(ROOT)}: {app_id} "
38
56
  f"({len(built.application.spec.tools)} tool)\n"