@flow-engineer/flow 0.0.1 → 0.2.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 (96) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +595 -0
  3. package/coding-agents/skill.md +321 -0
  4. package/coding-agents/test-skill.md +223 -0
  5. package/coding-agents/tools-skill.md +89 -0
  6. package/dist/flow-messaging/src/client.d.ts +34 -0
  7. package/dist/flow-messaging/src/client.js +130 -0
  8. package/dist/flow-messaging/src/index.d.ts +37 -0
  9. package/dist/flow-messaging/src/index.js +409 -0
  10. package/dist/simulators/src/index.d.ts +2 -0
  11. package/dist/simulators/src/index.js +2 -0
  12. package/dist/simulators/src/page.html +1245 -0
  13. package/dist/simulators/src/person.html +100 -0
  14. package/dist/simulators/src/platforms.d.ts +29 -0
  15. package/dist/simulators/src/platforms.js +72 -0
  16. package/dist/simulators/src/server.d.ts +201 -0
  17. package/dist/simulators/src/server.js +759 -0
  18. package/dist/src/agent.d.ts +504 -0
  19. package/dist/src/agent.js +818 -0
  20. package/dist/src/channels.d.ts +178 -0
  21. package/dist/src/channels.js +243 -0
  22. package/dist/src/conversation.d.ts +361 -0
  23. package/dist/src/conversation.js +1487 -0
  24. package/dist/src/db.d.ts +18 -0
  25. package/dist/src/db.js +57 -0
  26. package/dist/src/events.d.ts +17 -0
  27. package/dist/src/events.js +20 -0
  28. package/dist/src/http.d.ts +45 -0
  29. package/dist/src/http.js +269 -0
  30. package/dist/src/index.d.ts +14 -0
  31. package/dist/src/index.js +11 -0
  32. package/dist/src/model.d.ts +21 -0
  33. package/dist/src/model.js +96 -0
  34. package/dist/src/names.d.ts +17 -0
  35. package/dist/src/names.js +38 -0
  36. package/dist/src/nudges.d.ts +23 -0
  37. package/dist/src/nudges.js +117 -0
  38. package/dist/src/postgres.d.ts +20 -0
  39. package/dist/src/postgres.js +62 -0
  40. package/dist/src/schedules.d.ts +60 -0
  41. package/dist/src/schedules.js +249 -0
  42. package/dist/src/skills.d.ts +12 -0
  43. package/dist/src/skills.js +69 -0
  44. package/dist/src/store.d.ts +214 -0
  45. package/dist/src/store.js +448 -0
  46. package/dist/src/talk.d.ts +57 -0
  47. package/dist/src/talk.js +166 -0
  48. package/dist/src/time.d.ts +71 -0
  49. package/dist/src/time.js +170 -0
  50. package/dist/src/tools.d.ts +43 -0
  51. package/dist/src/tools.js +26 -0
  52. package/dist/src/traces.d.ts +31 -0
  53. package/dist/src/traces.js +303 -0
  54. package/dist/src/types.d.ts +260 -0
  55. package/dist/src/types.js +2 -0
  56. package/dist/testing/src/check-endpoint.d.ts +31 -0
  57. package/dist/testing/src/check-endpoint.js +158 -0
  58. package/dist/testing/src/chinwag.d.ts +2 -0
  59. package/dist/testing/src/chinwag.js +6 -0
  60. package/dist/testing/src/cli.d.ts +2 -0
  61. package/dist/testing/src/cli.js +563 -0
  62. package/dist/testing/src/client.d.ts +115 -0
  63. package/dist/testing/src/client.js +955 -0
  64. package/dist/testing/src/http-agent.d.ts +47 -0
  65. package/dist/testing/src/http-agent.js +155 -0
  66. package/dist/testing/src/index.d.ts +3 -0
  67. package/dist/testing/src/index.js +2 -0
  68. package/dist/testing/src/new.d.ts +9 -0
  69. package/dist/testing/src/new.js +122 -0
  70. package/dist/testing/src/print-run.d.ts +3 -0
  71. package/dist/testing/src/print-run.js +22 -0
  72. package/dist/testing/src/protocol.d.ts +159 -0
  73. package/dist/testing/src/protocol.js +28 -0
  74. package/dist/testing/src/report.d.ts +74 -0
  75. package/dist/testing/src/report.js +90 -0
  76. package/dist/testing/src/show.d.ts +36 -0
  77. package/dist/testing/src/show.js +280 -0
  78. package/dist/testing/src/suite.d.ts +87 -0
  79. package/dist/testing/src/suite.js +198 -0
  80. package/examples/cafe/agent/AGENTS.md +10 -0
  81. package/examples/cafe/agent/skills/allergens/SKILL.md +7 -0
  82. package/examples/cafe/src/agent.ts +39 -0
  83. package/examples/cafe/src/main.ts +16 -0
  84. package/examples/cafe/src/tools.ts +62 -0
  85. package/examples/cafe/tests/scenarios/latte-correction.yaml +30 -0
  86. package/examples/cafe/tests/scenarios/order-fails.yaml +26 -0
  87. package/examples/cafe/tests/tools/get_menu.yaml +14 -0
  88. package/examples/cafe/tests/tools/place_order.yaml +29 -0
  89. package/examples/http-agent/server.mjs +32 -0
  90. package/examples/http-agent/tests/scenarios/opening-hours.yaml +16 -0
  91. package/flow-messaging/README.md +152 -0
  92. package/package.json +62 -8
  93. package/spec/agent-endpoint.openapi.yaml +148 -0
  94. package/spec/nudges.openapi.yaml +167 -0
  95. package/spec/request.schema.json +121 -0
  96. package/spec/response.schema.json +284 -0
package/package.json CHANGED
@@ -1,13 +1,67 @@
1
1
  {
2
2
  "name": "@flow-engineer/flow",
3
- "version": "0.0.1",
4
- "description": "Flow Framework (coming soon)",
5
- "main": "index.js",
3
+ "version": "0.2.0",
4
+ "description": "An open-source runtime for chat agents that feel like a person texting back.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/flow-engineer/flow.git"
9
+ },
10
+ "homepage": "https://github.com/flow-engineer/flow",
11
+ "type": "module",
12
+ "engines": {
13
+ "node": ">=22.18"
14
+ },
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/src/index.d.ts",
18
+ "default": "./dist/src/index.js"
19
+ },
20
+ "./simulators": {
21
+ "types": "./dist/simulators/src/index.d.ts",
22
+ "default": "./dist/simulators/src/index.js"
23
+ },
24
+ "./flow-messaging": {
25
+ "types": "./dist/flow-messaging/src/index.d.ts",
26
+ "default": "./dist/flow-messaging/src/index.js"
27
+ },
28
+ "./testing": {
29
+ "types": "./dist/testing/src/index.d.ts",
30
+ "default": "./dist/testing/src/index.js"
31
+ },
32
+ "./package.json": "./package.json"
33
+ },
34
+ "bin": {
35
+ "flow": "dist/testing/src/cli.js",
36
+ "chinwag": "dist/testing/src/chinwag.js"
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "examples/cafe/agent",
41
+ "examples/cafe/src",
42
+ "examples/cafe/tests",
43
+ "examples/http-agent",
44
+ "flow-messaging/README.md",
45
+ "coding-agents",
46
+ "spec",
47
+ "README.md",
48
+ "LICENSE"
49
+ ],
6
50
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
51
+ "build": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json && node -e \"for (const f of ['page.html', 'person.html']) fs.copyFileSync('simulators/src/' + f, 'dist/simulators/src/' + f)\"",
52
+ "prepack": "npm run build",
53
+ "test": "node --test --test-timeout=30000 \"test/**/*.test.ts\"",
54
+ "test:changed": "node scripts/test-changed.ts",
55
+ "new": "node scripts/new-agent.ts",
56
+ "typecheck": "tsc --noEmit"
57
+ },
58
+ "dependencies": {
59
+ "openai": "^7.32.0",
60
+ "yaml": "^2.9.1",
61
+ "zod": "^4.6.5"
8
62
  },
9
- "keywords": [],
10
- "author": "",
11
- "license": "ISC",
12
- "type": "commonjs"
63
+ "devDependencies": {
64
+ "@types/node": "^26.6.5",
65
+ "typescript": "^7.0.2"
66
+ }
13
67
  }
@@ -0,0 +1,148 @@
1
+ openapi: 3.1.0
2
+ info:
3
+ title: Flow Framework agent endpoint
4
+ version: 0.1.0
5
+ license:
6
+ name: MIT
7
+ identifier: MIT
8
+ description: |
9
+ The HTTP endpoint an agent on any framework, in any language, exposes so Flow Framework's test suite
10
+ can talk to it: `flow test --agent-url <url>`. Implement this one operation and the test
11
+ cloud's simulated people can chat with your agent; `flow check-endpoint <url>` checks an
12
+ implementation against this file without a cloud, a key or a model call.
13
+
14
+ How a run uses it:
15
+ - Each of the person's messages is one POST to the URL you give, from the machine running
16
+ `flow test` (so `http://localhost` works and the agent never has to be reachable from the
17
+ internet). The path is yours; `/flow` is the examples'.
18
+ - A conversation's messages are sent one at a time, in order: the next is POSTed once the last
19
+ one's answer is in. Different conversations may be in flight at once.
20
+ - Keep each conversation's history by `conversationId`, as you would per phone number. The
21
+ request carries only the new message.
22
+ - Ignore request fields you do not know: later versions may add optional ones. The request
23
+ schema is exact for this version; do not reject requests that have more.
24
+ - Answer with status 200 and `{ "replies": [...] }`, the replies to send in order, in the same
25
+ HTTP response. `{ "replies": [] }` sends nothing (not 204, not an empty body). Answering
26
+ later, through a callback, is not supported.
27
+ - Your agent's own tools run on its side, for real: the suite's `tests/tools/*.yaml` simulators
28
+ are not used, and outcome checks see no tool calls, so write what must be true as `judge`
29
+ items.
30
+ - The person's reactions to the agent's messages are not sent in this version.
31
+
32
+ What fails a message's turn (the run records why and goes on to the next message):
33
+ - any status other than 200 (redirects are followed; the final status counts), with the
34
+ first 200 characters of the body in the reason;
35
+ - a 200 whose body is not JSON, or has no `replies` array;
36
+ - no answer within the timeout: 60 seconds by default, `--agent-timeout <s>` to change it;
37
+ - the URL cannot be reached.
38
+
39
+ A reply that is valid JSON but does not fit the run's platform (buttons on SMS, text over its
40
+ maxLength, an unknown key) is not sent; the run says why and the other replies still go.
41
+ `x-chinwag-platforms` below lists what each platform shows.
42
+ servers:
43
+ - url: http://localhost:3000
44
+ description: Wherever your agent listens; `flow test --agent-url` takes the full URL.
45
+ security:
46
+ - {}
47
+ - bearer: []
48
+ paths:
49
+ /flow:
50
+ post:
51
+ operationId: receiveMessage
52
+ summary: One message from the person; answer with the replies to send.
53
+ description: |
54
+ Sent with `content-type: application/json`. When FLOW_FRAMEWORK_AGENT_TOKEN is set where
55
+ `flow test` runs, every request carries `authorization: Bearer <token>`: check it, and
56
+ answer 401 to anything else.
57
+ requestBody:
58
+ required: true
59
+ content:
60
+ application/json:
61
+ schema:
62
+ $ref: ./request.schema.json
63
+ examples:
64
+ text:
65
+ summary: A typed message
66
+ value:
67
+ conversationId: whatsapp:ses_8f2a.opening-hours.whatsapp.1
68
+ messageId: m1
69
+ channel: whatsapp
70
+ message: { text: "hi, what time are you open today?" }
71
+ photo:
72
+ summary: A photo with no caption
73
+ value:
74
+ conversationId: whatsapp:ses_8f2a.receipt.whatsapp.1
75
+ messageId: m2
76
+ channel: whatsapp
77
+ message:
78
+ text: ""
79
+ attachments: [{ kind: image, mime: image/png, name: receipt.png, data: iVBORw0KGgo= }]
80
+ tap:
81
+ summary: A tapped button, replying to the agent's message
82
+ value:
83
+ conversationId: whatsapp:ses_8f2a.order.whatsapp.1
84
+ messageId: m3
85
+ channel: whatsapp
86
+ message:
87
+ text: Oat
88
+ choice: { title: Oat }
89
+ replyTo: { text: "Which milk?", fromAgent: true }
90
+ responses:
91
+ "200":
92
+ description: The replies to send, in order; `[]` is no reply.
93
+ content:
94
+ application/json:
95
+ schema:
96
+ $ref: ./response.schema.json
97
+ examples:
98
+ answer:
99
+ value:
100
+ replies:
101
+ - { type: reaction, emoji: "👍", messageId: m1 }
102
+ - { type: message, text: "We're open 8am to 6pm today.", buttons: [Order ahead] }
103
+ nothing:
104
+ value: { replies: [] }
105
+ default:
106
+ description: Any other status fails this message's turn; the body's first 200 characters are the reason shown in the run.
107
+ components:
108
+ securitySchemes:
109
+ bearer:
110
+ type: http
111
+ scheme: bearer
112
+ description: The value of FLOW_FRAMEWORK_AGENT_TOKEN, when it is set where `flow test` runs.
113
+ # What each platform a run plays shows, as ChannelInfo in src/types.ts: a reply is checked against
114
+ # its run's platform. A key that is absent means the platform does not have that feature. maxLength
115
+ # absent is 4,000. Kept in step with simulators/src/platforms.ts by test/check-endpoint.test.ts.
116
+ x-chinwag-platforms:
117
+ sms:
118
+ reactions: false
119
+ status: false
120
+ formatting: plain
121
+ maxLength: 1530
122
+ whatsapp:
123
+ reactions: true
124
+ status: false
125
+ formatting: whatsapp
126
+ maxLength: 4096
127
+ replies: true
128
+ buttons: { max: 3, labelMax: 20 }
129
+ lists: { maxRows: 10, titleMax: 24, buttonMax: 20 }
130
+ linkButton: { labelMax: 20 }
131
+ images: { captionMax: 1024 }
132
+ readReceipts: true
133
+ imessage:
134
+ reactions: true
135
+ status: false
136
+ formatting: plain
137
+ images: { captionMax: 4000 }
138
+ telegram:
139
+ reactions: true
140
+ status: false
141
+ formatting: markdown
142
+ images: { captionMax: 1024 }
143
+ web:
144
+ reactions: true
145
+ status: true
146
+ formatting: markdown
147
+ linkButton: { labelMax: 20 }
148
+ images: { captionMax: 4000 }
@@ -0,0 +1,167 @@
1
+ openapi: 3.1.0
2
+ info:
3
+ title: Flow Framework nudges over HTTP
4
+ version: 0.1.0
5
+ license:
6
+ name: MIT
7
+ identifier: MIT
8
+ description: |
9
+ The endpoint `nudgeHandler(agent, { token, base })` serves in the agent's own server, so a
10
+ backend that is a separate service can send nudges (messages the business starts) without a
11
+ route of its own. Paths are under `base`, `/nudges` by default; anything outside it gets 404.
12
+
13
+ - A POST answers as soon as the nudge is recorded, before it is sent or the agent's turn for it
14
+ runs: usually `queued`. Read how it ended with the GET.
15
+ - What is decided as it is recorded is in the POST's answer, still a 202: it is the nudge's
16
+ state, not an HTTP error. `refused` with `unknown_person` (they never wrote on that channel;
17
+ nothing is recorded, so `id` is "" and there is nothing to GET) or `outside_window` (past
18
+ WhatsApp's 24-hour window, for text and prompt nudges); `dropped` (they texted STOP);
19
+ `scheduled` (it has `at`).
20
+ - Every request needs `Authorization: Bearer <token>`. Wrong tokens are limited per address:
21
+ after 10 in 15 minutes the address gets 429 for the rest of that window, the right token too.
22
+ - Answers are JSON with `cache-control: no-store`.
23
+ servers:
24
+ - url: http://localhost:3000
25
+ description: Wherever the agent's server listens.
26
+ security:
27
+ - bearer: []
28
+ paths:
29
+ /nudges:
30
+ post:
31
+ operationId: recordNudge
32
+ summary: Record a nudge; answers at once with its id and state.
33
+ requestBody:
34
+ required: true
35
+ description: At most 16,384 bytes of JSON. Unknown fields are refused.
36
+ content:
37
+ application/json:
38
+ schema:
39
+ $ref: "#/components/schemas/NudgeRequest"
40
+ examples:
41
+ prompt:
42
+ summary: The agent writes it, in the context of the chat
43
+ value:
44
+ to: { channel: whatsapp, address: "+15550100100" }
45
+ prompt: Their order is ready for pickup.
46
+ idempotency_key: order-1042-ready
47
+ text:
48
+ summary: Sent as written, later
49
+ value:
50
+ to: { channel: sms, address: "+15550100100" }
51
+ text: Your table is booked for 7pm.
52
+ at: 1792335600000
53
+ responses:
54
+ "202":
55
+ description: Recorded (or, for an `idempotency_key` already used, the first nudge with that key).
56
+ content:
57
+ application/json:
58
+ schema:
59
+ $ref: "#/components/schemas/NudgeState"
60
+ examples:
61
+ queued:
62
+ value: { id: n_3f9c2a1b, status: queued }
63
+ unknownPerson:
64
+ value: { id: "", status: refused, reason: unknown_person }
65
+ "400":
66
+ description: The body is not JSON, too large, or does not fit; the channel is not one of the agent's; a declared template's values do not fit it.
67
+ content:
68
+ application/json:
69
+ schema:
70
+ $ref: "#/components/schemas/Error"
71
+ "401":
72
+ description: No token, or a wrong one.
73
+ content:
74
+ application/json:
75
+ schema:
76
+ $ref: "#/components/schemas/Error"
77
+ "429":
78
+ description: Too many wrong tokens from this address.
79
+ content:
80
+ application/json:
81
+ schema:
82
+ $ref: "#/components/schemas/Error"
83
+ /nudges/{id}:
84
+ get:
85
+ operationId: nudgeState
86
+ summary: A nudge's state now.
87
+ parameters:
88
+ - name: id
89
+ in: path
90
+ required: true
91
+ schema: { type: string }
92
+ responses:
93
+ "200":
94
+ description: Its state now.
95
+ content:
96
+ application/json:
97
+ schema:
98
+ $ref: "#/components/schemas/NudgeState"
99
+ example: { id: n_3f9c2a1b, status: sent }
100
+ "401":
101
+ description: No token, or a wrong one.
102
+ "404":
103
+ description: The agent never recorded a nudge with this id.
104
+ "429":
105
+ description: Too many wrong tokens from this address.
106
+ components:
107
+ securitySchemes:
108
+ bearer:
109
+ type: http
110
+ scheme: bearer
111
+ description: The token given to `nudgeHandler`, at least 32 characters.
112
+ schemas:
113
+ NudgeRequest:
114
+ type: object
115
+ additionalProperties: false
116
+ required: [to]
117
+ description: Exactly one of `text`, `prompt` or `template`.
118
+ properties:
119
+ to:
120
+ type: object
121
+ additionalProperties: false
122
+ required: [channel, address]
123
+ description: The person, by a channel identity they have written from.
124
+ properties:
125
+ channel: { type: string, description: One of the agent's channels, by name. }
126
+ address: { type: string, description: "Their address on it: a phone number, or the custom UI's user id." }
127
+ text: { type: string, minLength: 1, description: Sent as written, with no model call. }
128
+ prompt: { type: string, minLength: 1, description: The agent gets a turn with this note and writes the message itself, or sends nothing. }
129
+ template:
130
+ type: object
131
+ additionalProperties: false
132
+ required: [name]
133
+ description: A WhatsApp template the owner set up with Meta, as `Template` in src/types.ts. Cannot be combined with `at` yet.
134
+ properties:
135
+ name: { type: string }
136
+ language: { type: string }
137
+ header:
138
+ oneOf:
139
+ - { type: object, additionalProperties: false, required: [kind, text], properties: { kind: { const: text }, text: { type: string } } }
140
+ - { type: object, additionalProperties: false, required: [kind], properties: { kind: { enum: [image, video, document] }, url: { type: string }, fileId: { type: string } } }
141
+ body: { type: array, items: { type: string } }
142
+ buttons:
143
+ type: array
144
+ items: { type: object, additionalProperties: false, required: [index, value], properties: { index: { type: integer, minimum: 0 }, value: { type: string } } }
145
+ variables: { type: object, additionalProperties: { type: string } }
146
+ at: { type: integer, minimum: 0, description: Send it later, at this time, in milliseconds since the epoch. }
147
+ idempotency_key: { type: string, minLength: 1, maxLength: 200, description: The same key twice sends once; the second call gets the first nudge's id and state. }
148
+ respect_quiet_hours: { type: boolean, description: "Wait for the end of the person's quiet hours (default true); false sends it now." }
149
+ expires_at: { type: integer, minimum: 0, description: "Milliseconds since the epoch: held for quiet hours past this, it is dropped as expired (default 24 hours on)." }
150
+ NudgeState:
151
+ type: object
152
+ required: [id, status]
153
+ properties:
154
+ id: { type: string }
155
+ status:
156
+ enum: [queued, sent, silent, dropped, refused, failed, scheduled, held, expired]
157
+ description: |
158
+ queued (waiting for its send or turn); sent; silent (the agent chose to send nothing);
159
+ dropped (a no-contact rule covers it, or their STOP); refused (outside_window,
160
+ unknown_person); failed (it could not be sent, or the model failed); scheduled; held (their
161
+ quiet hours, until they end or the person writes); expired (held past expires_at).
162
+ reason: { type: string, description: Why, for dropped, refused and failed. }
163
+ Error:
164
+ type: object
165
+ required: [error]
166
+ properties:
167
+ error: { type: string }
@@ -0,0 +1,121 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/flow-engineer/flow/blob/main/spec/request.schema.json",
4
+ "title": "AgentRequest",
5
+ "description": "One of the person's messages, as `flow test --agent-url` POSTs it to the agent's endpoint. This schema lists exactly what this version sends (additionalProperties is false so it stays exact); later versions may add optional fields, so an endpoint should ignore fields it does not know rather than validate requests strictly against this file.",
6
+ "type": "object",
7
+ "required": [
8
+ "conversationId",
9
+ "messageId",
10
+ "channel",
11
+ "message"
12
+ ],
13
+ "additionalProperties": false,
14
+ "properties": {
15
+ "conversationId": {
16
+ "type": "string",
17
+ "minLength": 1,
18
+ "description": "The conversation, as a phone number or chat would be: keep the history by it. In a test run it is \"<platform>:<session>.<scenario>.<platform>.<run>\", such as \"whatsapp:ses_8f2a.opening-hours.whatsapp.1\"."
19
+ },
20
+ "messageId": {
21
+ "type": "string",
22
+ "description": "The id this message has in the conversation (m1, m2, ...): what a reply's `replyTo`, a reaction's `messageId` or a read's `messageIds` names."
23
+ },
24
+ "channel": {
25
+ "type": "string",
26
+ "minLength": 1,
27
+ "description": "The platform the run plays (whatsapp, sms, imessage, telegram, web), or the name given with --channel."
28
+ },
29
+ "message": {
30
+ "$ref": "#/$defs/message"
31
+ }
32
+ },
33
+ "$defs": {
34
+ "message": {
35
+ "type": "object",
36
+ "required": [
37
+ "text"
38
+ ],
39
+ "additionalProperties": false,
40
+ "properties": {
41
+ "text": {
42
+ "type": "string",
43
+ "description": "What the person wrote; \"\" when they only sent a file (then it is the file's caption, if any)."
44
+ },
45
+ "replyTo": {
46
+ "type": "object",
47
+ "description": "The message they replied to (theirs or the agent's), quoted.",
48
+ "required": [
49
+ "text",
50
+ "fromAgent"
51
+ ],
52
+ "additionalProperties": false,
53
+ "properties": {
54
+ "text": {
55
+ "type": "string"
56
+ },
57
+ "fromAgent": {
58
+ "type": "boolean",
59
+ "description": "true when the quoted message is the agent's."
60
+ }
61
+ }
62
+ },
63
+ "choice": {
64
+ "type": "object",
65
+ "description": "They tapped a button or picked a list row rather than typing.",
66
+ "required": [
67
+ "title"
68
+ ],
69
+ "additionalProperties": false,
70
+ "properties": {
71
+ "id": {
72
+ "type": "string",
73
+ "description": "The list row's id, when it has one."
74
+ },
75
+ "title": {
76
+ "type": "string"
77
+ }
78
+ }
79
+ },
80
+ "attachments": {
81
+ "type": "array",
82
+ "description": "Photos, voice notes and documents sent with the message.",
83
+ "items": {
84
+ "$ref": "#/$defs/attachment"
85
+ }
86
+ }
87
+ }
88
+ },
89
+ "attachment": {
90
+ "type": "object",
91
+ "required": [
92
+ "kind",
93
+ "mime",
94
+ "data"
95
+ ],
96
+ "additionalProperties": false,
97
+ "properties": {
98
+ "kind": {
99
+ "enum": [
100
+ "image",
101
+ "audio",
102
+ "document"
103
+ ]
104
+ },
105
+ "mime": {
106
+ "type": "string",
107
+ "description": "Such as image/jpeg, audio/ogg, application/pdf."
108
+ },
109
+ "name": {
110
+ "type": "string",
111
+ "description": "The file name, when the platform gives one (documents)."
112
+ },
113
+ "data": {
114
+ "type": "string",
115
+ "contentEncoding": "base64",
116
+ "description": "The file's bytes, base64 (standard alphabet, with padding)."
117
+ }
118
+ }
119
+ }
120
+ }
121
+ }