@flow-engineer/flow 0.2.1 → 0.2.3

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 (70) hide show
  1. package/README.md +167 -10
  2. package/coding-agents/auth-skill.md +137 -0
  3. package/coding-agents/context-skill.md +94 -0
  4. package/coding-agents/skill.md +219 -17
  5. package/coding-agents/test-skill.md +3 -1
  6. package/coding-agents/tools-skill.md +78 -1
  7. package/dist/cli/src/auth.d.ts +11 -0
  8. package/dist/cli/src/auth.js +147 -0
  9. package/dist/cli/src/commands.d.ts +119 -0
  10. package/dist/cli/src/commands.js +212 -0
  11. package/dist/cli/src/index.d.ts +2 -0
  12. package/dist/cli/src/index.js +5 -0
  13. package/dist/cli/src/run.d.ts +28 -0
  14. package/dist/cli/src/run.js +523 -0
  15. package/dist/cli/src/terminal.d.ts +3 -0
  16. package/dist/cli/src/terminal.js +135 -0
  17. package/dist/flow-messaging/src/client.d.ts +2 -2
  18. package/dist/flow-messaging/src/client.js +3 -3
  19. package/dist/flow-messaging/src/index.js +17 -9
  20. package/dist/mcp/src/client.d.ts +54 -0
  21. package/dist/mcp/src/client.js +175 -0
  22. package/dist/mcp/src/describe.d.ts +41 -0
  23. package/dist/mcp/src/describe.js +110 -0
  24. package/dist/mcp/src/index.d.ts +4 -0
  25. package/dist/mcp/src/index.js +7 -0
  26. package/dist/mcp/src/server.d.ts +70 -0
  27. package/dist/mcp/src/server.js +252 -0
  28. package/dist/mcp/src/terminal.d.ts +24 -0
  29. package/dist/mcp/src/terminal.js +54 -0
  30. package/dist/simulators/src/index.d.ts +1 -1
  31. package/dist/simulators/src/page.html +267 -49
  32. package/dist/simulators/src/server.d.ts +27 -0
  33. package/dist/simulators/src/server.js +264 -40
  34. package/dist/src/agent.d.ts +69 -3
  35. package/dist/src/agent.js +162 -8
  36. package/dist/src/channels.d.ts +6 -4
  37. package/dist/src/channels.js +4 -3
  38. package/dist/src/conversation.d.ts +17 -1
  39. package/dist/src/conversation.js +170 -16
  40. package/dist/src/events.d.ts +1 -1
  41. package/dist/src/index.d.ts +2 -2
  42. package/dist/src/index.js +1 -1
  43. package/dist/src/model.d.ts +27 -0
  44. package/dist/src/model.js +85 -13
  45. package/dist/src/skills.d.ts +4 -0
  46. package/dist/src/skills.js +23 -0
  47. package/dist/src/store.d.ts +2 -0
  48. package/dist/src/store.js +18 -0
  49. package/dist/src/tools.d.ts +69 -0
  50. package/dist/src/tools.js +17 -1
  51. package/dist/src/traces.js +32 -4
  52. package/dist/src/types.d.ts +5 -0
  53. package/dist/testing/src/account.d.ts +1 -1
  54. package/dist/testing/src/account.js +368 -57
  55. package/dist/testing/src/chat.js +24 -16
  56. package/dist/testing/src/check.d.ts +4 -0
  57. package/dist/testing/src/check.js +90 -0
  58. package/dist/testing/src/cli.js +77 -19
  59. package/dist/testing/src/flow-login.d.ts +33 -0
  60. package/dist/testing/src/flow-login.js +30 -5
  61. package/dist/testing/src/new.d.ts +14 -5
  62. package/dist/testing/src/new.js +54 -20
  63. package/dist/testing/src/plugin.d.ts +4 -0
  64. package/dist/testing/src/plugin.js +116 -0
  65. package/docs/agents.md +87 -12
  66. package/examples/cafe/src/cli.ts +24 -0
  67. package/examples/cafe/src/mcp.ts +22 -0
  68. package/examples/cafe/src/tools.ts +13 -0
  69. package/flow-messaging/README.md +3 -1
  70. package/package.json +28 -1
package/dist/src/agent.js CHANGED
@@ -14,7 +14,7 @@
14
14
  // every send to it (replies, scheduled messages, nudges) goes back the same way.
15
15
  import { randomUUID } from "node:crypto";
16
16
  import { declaredTemplate, fillTemplate, keywordOf, summarize } from "./channels.js";
17
- import { Conversation, PROFILE_TOOL, profileTool, transcriptOf, windowClosed } from "./conversation.js";
17
+ import { Conversation, PROFILE_TOOL, profileTool, signInNotice, transcriptOf, windowClosed } from "./conversation.js";
18
18
  import { sqlite } from "./db.js";
19
19
  import { emitter } from "./events.js";
20
20
  import { businessSchedule, claimDue, summary } from "./schedules.js";
@@ -36,6 +36,8 @@ export class Agent {
36
36
  /** Conversations in the store, known once resume() ran. */
37
37
  rows = new Map();
38
38
  resumed = false;
39
+ requestReplies = new Map();
40
+ requestCredentials = new Map();
39
41
  arriving = new Map();
40
42
  /** Callers waiting for a queued nudge to settle: its first call, and any with the same key. */
41
43
  nudgeWaiters = new Map();
@@ -44,6 +46,7 @@ export class Agent {
44
46
  /** Where each identity (channel, address) last wrote from; sends go back the same way. */
45
47
  via = new Map();
46
48
  playgroundSink = null;
49
+ playgroundVisitors = new Map();
47
50
  /** Before this, this runner does not ask for the pruning claim again (it, or another runner, had it). */
48
51
  pruneAfter = 0;
49
52
  /** `defaultCountryCode`'s digits. */
@@ -72,7 +75,7 @@ export class Agent {
72
75
  throw new Error(`flow: defaultCountryCode is a calling code such as "+91", not "${options.defaultCountryCode}"`);
73
76
  this.countryCode = options.defaultCountryCode?.trim().replace(/^\+/, "");
74
77
  const tracing = options.traces ?? tracesFromEnv();
75
- this.traces = tracing ? traces(tracing, { service: options.id ?? "flow", kindOf: (name) => this.channelsByName.get(name)?.kind }) : null;
78
+ this.traces = tracing ? traces(tracing, { service: options.id ?? "flow", kindOf: (name) => this.channelsByName.get(name)?.traceChannel ?? this.channelsByName.get(name)?.kind }) : null;
76
79
  const sinks = [...(options.events ?? []), ...(this.traces ? [this.traces.sink] : [])];
77
80
  const emit = emitter(sinks);
78
81
  if ((options.dir === undefined) === (options.files === undefined))
@@ -92,6 +95,9 @@ export class Agent {
92
95
  model: options.model,
93
96
  transcribe: options.transcribe,
94
97
  tools: new Map(tools.map((t) => [t.name, t])),
98
+ credentials: options.credentials,
99
+ credentialProvider: ctx => this.requestCredentials.get(viaKey(ctx.channel, ctx.address ?? "")) ?? options.credentials,
100
+ auth: options.auth,
95
101
  specs: tools.map(toSpec),
96
102
  instructions: loadInstructions(files, skills, options.inlineSkills),
97
103
  context: options.context,
@@ -145,6 +151,33 @@ export class Agent {
145
151
  async message(input) {
146
152
  return this.released(await this.take(this.channelOf(input.channel), { ...input, via: "provider" }));
147
153
  }
154
+ /**
155
+ * A signed-in HTTP channel's request. `key` must come from verified auth, never the body.
156
+ * Uses the same person as resolve() on other channels. The caller serializes requests per key.
157
+ */
158
+ async request(input) {
159
+ await this.settings.ready;
160
+ const adapter = this.channelOf(input.channel);
161
+ if (adapter.kind !== "custom")
162
+ throw new Error("chinwag: request needs a custom UI channel");
163
+ const identity = await this.conversationFor(adapter, input.channel, input.key, undefined, input.key);
164
+ if (input.conversationId && input.conversationId !== identity.conversationId)
165
+ throw new Error("chinwag: conversation does not belong to this person");
166
+ const replies = [];
167
+ const address = viaKey(input.channel, input.key);
168
+ if (input.credentials)
169
+ this.requestCredentials.set(address, input.credentials);
170
+ this.requestReplies.set(address, (reply) => { replies.push(reply); });
171
+ try {
172
+ await this.released(await this.take(adapter, { channel: input.channel, from: input.key, message: input.text, via: "provider", verifiedKey: input.key }));
173
+ await this.conversation(identity.conversationId)?.whenIdle();
174
+ return { conversationId: identity.conversationId, replies };
175
+ }
176
+ finally {
177
+ this.requestReplies.delete(address);
178
+ this.requestCredentials.delete(address);
179
+ }
180
+ }
148
181
  /**
149
182
  * A message from a test person (by their phone number) in the test playground: exactly what
150
183
  * message() does for the same person writing on that channel, but its replies go to the
@@ -157,6 +190,106 @@ export class Agent {
157
190
  const adapter = this.channelOf(input.channel);
158
191
  return this.released(await this.take(adapter, { channel: input.channel, from: this.testAddress(adapter, person), message: input.message, messageId: input.messageId, via: "playground", person }));
159
192
  }
193
+ /**
194
+ * A playground visitor, signed out or authenticated. `key` is verified by server code, never
195
+ * accepted from the browser. Replies stay in the simulator; no provider send is made.
196
+ */
197
+ async simulateVisitor(input) {
198
+ if (!input.address)
199
+ throw new Error("chinwag: a playground visitor needs a server-issued channel address");
200
+ return this.released(await this.take(this.channelOf(input.channel), { channel: input.channel, from: input.key ? input.address : `visitor:${input.address}`, message: input.message, via: "playground", verifiedKey: input.key ?? undefined, visitor: input.key ? undefined : input.address }));
201
+ }
202
+ /**
203
+ * A completed server-verified login. Bind its channel address to the account before sending a
204
+ * confirmation with recordNudge/nudge. Never take key from a browser body or model input.
205
+ * Anonymous history stays separate; an identity belonging to another account is refused.
206
+ */
207
+ async linkIdentity(input) {
208
+ if (!input.address || !input.key)
209
+ throw new Error("chinwag: linking needs a verified account and channel address");
210
+ const adapter = this.channelOf(input.channel);
211
+ const lock = viaKey(input.channel, input.address);
212
+ const work = (this.arriving.get(lock) ?? Promise.resolve()).then(async () => {
213
+ await this.settings.ready;
214
+ const personKey = `key:${input.key}`;
215
+ let row = await this.store.conversationByKey(personKey);
216
+ if (!row) {
217
+ await this.store.createConversation(`p_${randomUUID().slice(0, 12)}`, personKey);
218
+ row = await this.store.conversationByKey(personKey);
219
+ }
220
+ if (!row)
221
+ throw new Error("chinwag: could not open the verified account conversation");
222
+ if (!await this.store.linkIdentity(row.id, input.channel, input.address))
223
+ throw new Error("chinwag: channel identity belongs to a different person");
224
+ const identity = (await this.store.identity(input.channel, input.address));
225
+ const via = input.via ?? identity.via;
226
+ if (identity.via !== via)
227
+ await this.store.setIdentityVia(input.channel, input.address, via);
228
+ this.via.set(lock, via);
229
+ if (via === "playground")
230
+ this.playgroundVisitors.set(lock, { key: input.key });
231
+ this.rows.set(row.id, row);
232
+ this.open(row.id).link({ name: input.channel, info: adapter.info, params: adapter.params, instructions: adapter.instructions, address: input.address, deliver: this.deliverer(adapter, input.address, row.id), lastMessageAt: identity.lastMessageAt });
233
+ return { conversationId: row.id };
234
+ });
235
+ this.arriving.set(lock, work.catch(() => undefined));
236
+ return work;
237
+ }
238
+ /** Wake the agent after verified sign-in; a repeated completion queues one internal event. */
239
+ async signInCompleted(input) {
240
+ await this.settings.ready;
241
+ if (!input.idempotencyKey)
242
+ throw new Error("chinwag: sign-in completion needs an idempotency key");
243
+ const earlier = await this.store.nudgeByKey(input.idempotencyKey);
244
+ if (earlier) {
245
+ const linked = await this.linkIdentity(input);
246
+ if (earlier.conversationId !== linked.conversationId || earlier.channel !== input.channel || earlier.body.address !== input.address)
247
+ throw new Error("chinwag: sign-in completion belongs to a different identity");
248
+ return this.result(earlier.id, earlier.status, earlier.reason);
249
+ }
250
+ const known = await this.store.identity(input.channel, input.address);
251
+ const anonymous = await this.store.identity(input.channel, `visitor:${input.address}`);
252
+ let source = input.conversationId ? [known, anonymous].find(i => i?.conversationId === input.conversationId) ?? null : known ?? anonymous;
253
+ if (input.conversationId && !source) {
254
+ // A previous attempt may have linked this address before the internal event could be queued.
255
+ const row = await this.store.conversation(input.conversationId);
256
+ const adapter = this.channelOf(input.channel);
257
+ const phone = adapter.phone ? normalizePhone(input.address, adapter.kind === "whatsapp" ? undefined : this.countryCode) : "";
258
+ const allowed = [`key:${input.key}`, `${input.channel}:${input.address}`, `${input.channel}:visitor:${input.address}`, ...(phone ? [`phone:${phone}`] : [])];
259
+ if (!row || !allowed.includes(row.personKey))
260
+ throw new Error("chinwag: sign-in context does not belong to this channel address");
261
+ source = { conversationId: row.id };
262
+ }
263
+ const history = source ? await this.transcript(source.conversationId) : [];
264
+ const latest = history.findLastIndex(e => e.kind === "in" && e.channel === input.channel);
265
+ const request = latest >= 0 ? history[latest] : undefined;
266
+ const after = history.slice(latest + 1);
267
+ const blocked = request?.kind === "in" && after.some(e => e.kind === "out" && e.channel === input.channel && e.authentication) && after.some(e => {
268
+ if (e.kind !== "tool")
269
+ return false;
270
+ try {
271
+ const output = JSON.parse(e.output);
272
+ return output.error === "sign_in_required" && output.executed === false;
273
+ }
274
+ catch {
275
+ return false;
276
+ }
277
+ });
278
+ const { conversationId } = await this.linkIdentity(input);
279
+ const account = source?.conversationId === conversationId ? history : await this.transcript(conversationId);
280
+ const newer = request && account.some(e => e.kind === "in" && e.at > request.at);
281
+ return this.recordNudge({ to: { channel: input.channel, address: input.address },
282
+ prompt: signInNotice(input.channel, blocked && !newer && request?.kind === "in" ? request.text : undefined),
283
+ idempotencyKey: input.idempotencyKey, respectQuietHours: false,
284
+ });
285
+ }
286
+ /** A verified playground visitor's account history, including messages on other interfaces. */
287
+ async visitorConversation(key, identity) {
288
+ await this.settings.ready;
289
+ if (!key)
290
+ return identity ? (await this.store.identity(identity.channel, `visitor:${identity.address}`))?.conversationId ?? null : null;
291
+ return (await this.store.conversationByKey(`key:${key}`))?.id ?? null;
292
+ }
160
293
  /** They wrote, so they are there: what was held for their quiet hours goes now, in this turn. */
161
294
  async released(taken) {
162
295
  if (taken)
@@ -254,7 +387,14 @@ export class Agent {
254
387
  }
255
388
  async arrive(adapter, input) {
256
389
  await this.settings.ready;
257
- const identity = await this.conversationFor(adapter, input.channel, input.from, input.person);
390
+ const identity = await this.conversationFor(adapter, input.channel, input.from, input.person, input.verifiedKey);
391
+ if (input.via === "playground") {
392
+ const address = viaKey(input.channel, input.from);
393
+ if (input.verifiedKey || input.visitor)
394
+ this.playgroundVisitors.set(address, { key: input.verifiedKey, visitor: input.visitor });
395
+ else
396
+ this.playgroundVisitors.delete(address);
397
+ }
258
398
  const conversationId = identity.conversationId;
259
399
  if (input.messageId !== undefined && (await this.store.inboundTaken(conversationId, input.channel, input.messageId))) {
260
400
  // The provider sent this message before (a webhook retried): it was taken then. Its id is
@@ -309,10 +449,13 @@ export class Agent {
309
449
  return { reply, ...conversation.keyword(kind, text, channel, messageId) };
310
450
  }
311
451
  /** The identity a message comes from, made (and linked to its person's conversation) the first time it writes. */
312
- async conversationFor(adapter, channel, address, testPerson) {
452
+ async conversationFor(adapter, channel, address, testPerson, verifiedKey) {
313
453
  const known = await this.store.identity(channel, address);
314
- if (known)
454
+ if (known) {
455
+ if (verifiedKey && (await this.store.conversation(known.conversationId))?.personKey !== `key:${verifiedKey}`)
456
+ throw new Error("chinwag: channel identity belongs to a different person");
315
457
  return known;
458
+ }
316
459
  let personKey;
317
460
  let verifiedBy;
318
461
  if (testPerson && !adapter.phone) {
@@ -320,8 +463,8 @@ export class Agent {
320
463
  [personKey, verifiedBy] = [await this.testPersonKey(testPerson, adapter), "test person"];
321
464
  }
322
465
  else {
323
- const owner = await this.options.resolve?.({ channel, address });
324
466
  const phone = adapter.phone ? normalizePhone(address, adapter.kind === "whatsapp" ? undefined : this.countryCode) : "";
467
+ const owner = verifiedKey ?? await this.options.resolve?.({ channel, address, ...(phone ? { phone } : {}) });
325
468
  [personKey, verifiedBy] = owner ? [`key:${owner}`, "resolver"] : phone ? [`phone:${phone}`, "phone"] : [`${channel}:${address}`, "channel"];
326
469
  }
327
470
  let row = await this.store.conversationByKey(personKey);
@@ -347,7 +490,7 @@ export class Agent {
347
490
  async testPersonKey(person, adapter) {
348
491
  const phone = normalizePhone(person.phone);
349
492
  const channel = [...this.channelsByName.values()].find((c) => c.phone) ?? adapter;
350
- const owner = channel ? await this.options.resolve?.({ channel: channel.name, address: phone }) : null;
493
+ const owner = channel ? await this.options.resolve?.({ channel: channel.name, address: phone, ...(channel.phone ? { phone } : {}) }) : null;
351
494
  return owner ? `key:${owner}` : `phone:${phone}`;
352
495
  }
353
496
  testPerson(phone) {
@@ -362,13 +505,24 @@ export class Agent {
362
505
  /** Sends to one identity: to the playground if it last wrote from there, else through the channel's provider. */
363
506
  deliverer(adapter, address, conversationId) {
364
507
  return (outbound) => {
508
+ const reply = this.requestReplies.get(viaKey(adapter.name, address));
509
+ if (reply)
510
+ return reply(outbound);
365
511
  if (this.via.get(viaKey(adapter.name, address)) !== "playground")
366
512
  return adapter.send(address, outbound);
513
+ const visitor = this.playgroundVisitors.get(viaKey(adapter.name, address));
514
+ if (visitor)
515
+ return this.playgroundSink?.({ conversationId, person: { name: visitor.key ? "Signed-in user" : "New user", phone: "", ...visitor }, channel: adapter.name, outbound });
367
516
  const phone = address.startsWith("test:") ? address.slice(5) : address;
368
517
  const person = this.testPerson(phone);
369
518
  // With no playground page open, nothing is shown, and the conversation keeps it as sent.
370
519
  if (person)
371
520
  return this.playgroundSink?.({ conversationId, person, channel: adapter.name, outbound });
521
+ const key = this.rows.get(conversationId)?.personKey;
522
+ if (key?.startsWith("key:"))
523
+ return this.playgroundSink?.({ conversationId, person: { name: "Signed-in user", phone: "", key: key.slice(4) }, channel: adapter.name, outbound });
524
+ if (address.startsWith("visitor:"))
525
+ return this.playgroundSink?.({ conversationId, person: { name: "New user", phone: "", visitor: address.slice(8) }, channel: adapter.name, outbound });
372
526
  };
373
527
  }
374
528
  /**
@@ -601,7 +755,7 @@ export class Agent {
601
755
  const added = await add({ ...record, body: { text: input.text, prompt: input.prompt, at: input.at }, status: "scheduled" }, schedule);
602
756
  return typeof added === "number" ? { id, status: "scheduled" } : added;
603
757
  }
604
- const body = input.template ? { template: input.template } : input.text !== undefined ? { text: input.text } : { prompt: input.prompt };
758
+ const body = { address: input.to.address, ...(input.template ? { template: input.template } : input.text !== undefined ? { text: input.text } : { prompt: input.prompt }) };
605
759
  const now = this.now();
606
760
  const quietEnds = input.respectQuietHours === false ? null : await this.quietEnd(conversationId, now);
607
761
  if (quietEnds !== null) {
@@ -6,6 +6,8 @@ export interface Channel {
6
6
  /** The agent's name for it; its send tool is always send_<name> (lowercase, other characters as _). "key" and "phone" are reserved. */
7
7
  name: string;
8
8
  kind: ChannelKind;
9
+ /** Override the trace channel classification, e.g. a terminal custom UI as cli. */
10
+ traceChannel?: string;
9
11
  info: ChannelInfo;
10
12
  /** Addresses are phone numbers the channel verified, so the same number on two channels is one person. */
11
13
  phone?: boolean;
@@ -74,6 +76,8 @@ export interface Message {
74
76
  image?: {
75
77
  url: string;
76
78
  };
79
+ /** Given for a send the business starts (a nudge, a scheduled text): the same in every process, so a provider that takes one sends it once. */
80
+ idempotencyKey?: string;
77
81
  }
78
82
  export interface WhatsAppProvider {
79
83
  sendMessage(to: string, message: Message): Promise<void>;
@@ -88,12 +92,10 @@ export interface WhatsAppProvider {
88
92
  react?(to: string, messageId: string, emoji: string): Promise<void>;
89
93
  typing?(to: string, on: boolean): Promise<void>;
90
94
  markRead?(to: string, messageIds: string[]): Promise<void>;
91
- sendTemplate?(to: string, template: Template): Promise<void>;
95
+ sendTemplate?(to: string, template: Template, idempotencyKey?: string): Promise<void>;
92
96
  }
93
97
  export interface SmsProvider {
94
- sendMessage(to: string, message: {
95
- text: string;
96
- }): Promise<void>;
98
+ sendMessage(to: string, message: Pick<Message, "text" | "idempotencyKey">): Promise<void>;
97
99
  }
98
100
  /** Your own UI. `message` carries the text, any buttons, and your params by name. */
99
101
  export interface CustomUIProvider {
@@ -93,8 +93,8 @@ export function whatsapp(provider, options = {}) {
93
93
  case "template": {
94
94
  if (!provider.sendTemplate || off.has("templates"))
95
95
  throw new Error("this WhatsApp provider sends no templates");
96
- const { type: _type, ...template } = outbound;
97
- return provider.sendTemplate(to, template);
96
+ const { type: _type, idempotencyKey, ...template } = outbound;
97
+ return provider.sendTemplate(to, template, idempotencyKey);
98
98
  }
99
99
  case "status":
100
100
  return;
@@ -112,7 +112,7 @@ export function sms(provider, options = {}) {
112
112
  phone: true,
113
113
  send: (to, outbound) => {
114
114
  if (outbound.type === "message")
115
- return provider.sendMessage(to, { text: outbound.text });
115
+ return provider.sendMessage(to, { text: outbound.text, ...(outbound.idempotencyKey ? { idempotencyKey: outbound.idempotencyKey } : {}) });
116
116
  if (outbound.type === "template")
117
117
  throw new Error("SMS has no templates");
118
118
  // Reactions, typing, read receipts and status do not exist on SMS.
@@ -159,6 +159,7 @@ function message(outbound) {
159
159
  ...(outbound.list ? { list: outbound.list } : {}),
160
160
  ...(outbound.link ? { link: outbound.link } : {}),
161
161
  ...(outbound.image ? { image: outbound.image } : {}),
162
+ ...(outbound.idempotencyKey ? { idempotencyKey: outbound.idempotencyKey } : {}),
162
163
  };
163
164
  }
164
165
  /**
@@ -2,13 +2,16 @@ import { z } from "zod";
2
2
  import type { Emit, EventSink } from "./events.ts";
3
3
  import type { ConversationRow, InboxRow, QuietHours, StoreApi, TimeZoneSource } from "./store.ts";
4
4
  import { type TalkSettings } from "./talk.ts";
5
- import { type Tool } from "./tools.ts";
5
+ import { type ChatAuth, type CredentialProvider, type Tool, type ToolContext } from "./tools.ts";
6
6
  import type { ChannelInfo, ChatMessage, Deliver, Incoming, Model, TemplateDefinition, ToolSpec, Transcribe } from "./types.ts";
7
7
  export interface ConversationSettings {
8
8
  model: Model;
9
9
  /** Speech to text for voice notes; without it the agent is told it can't listen to them. */
10
10
  transcribe?: Transcribe;
11
11
  tools: Map<string, Tool>;
12
+ credentials?: CredentialProvider;
13
+ credentialProvider?: (context: Readonly<Omit<ToolContext, "credentials">>) => CredentialProvider | undefined;
14
+ auth?: ChatAuth;
12
15
  specs: ToolSpec[];
13
16
  instructions: string;
14
17
  /** Per-turn context for the system prompt (the person's profile, the time). */
@@ -68,6 +71,8 @@ export interface ChannelLink {
68
71
  deliver: Deliver;
69
72
  lastMessageAt: number | null;
70
73
  }
74
+ /** A server-verified auth change: an internal prompt, never a prewritten chat response. */
75
+ export declare function signInNotice(channel: string, request?: string): string;
71
76
  interface Task {
72
77
  id: string;
73
78
  tool: string;
@@ -98,6 +103,7 @@ export declare class Conversation {
98
103
  /** Aborted when the conversation closes: tools' signal. */
99
104
  private readonly lifetime;
100
105
  private recentChannels;
106
+ private promptDestination?;
101
107
  private timeZone;
102
108
  /** Their own quiet hours ("off": none), or null for the agent's. */
103
109
  private quietHours;
@@ -117,6 +123,10 @@ export declare class Conversation {
117
123
  /** The next model response is the first since their latest message. */
118
124
  private firstResponse;
119
125
  private readonly typingOn;
126
+ /** One challenge per channel per turn, even when the model calls several protected tools. */
127
+ private signInPrompts;
128
+ private authBlocked;
129
+ private authPromptsSent;
120
130
  /** The send tool for each channel, this turn. */
121
131
  private sendTools;
122
132
  private readonly builtins;
@@ -258,6 +268,7 @@ export declare class Conversation {
258
268
  private send;
259
269
  /** A fixed-text scheduled message or a text or template nudge, sent with no model turn. */
260
270
  private sendDirect;
271
+ private directSettled;
261
272
  /**
262
273
  * The history the model is sent: all of it while it fits the budget, else the latest entries
263
274
  * that do, from the start of one (never between a call and its result), after a line that says
@@ -276,6 +287,10 @@ export declare class Conversation {
276
287
  private runTools;
277
288
  /** Start one tool call; the model's input is checked first (a refusal is the call's result). */
278
289
  private start;
290
+ /** A failed or unresponsive auth service cannot leave a protected call hanging or let it execute. */
291
+ private authentication;
292
+ /** A login request is a real channel message, recorded as delivered, without another model call. */
293
+ private sendSignIn;
279
294
  /** The reply stops waiting for this tool: it carries on in the background and its result comes back as a note. */
280
295
  private detach;
281
296
  }
@@ -296,6 +311,7 @@ export type TranscriptEntry = {
296
311
  kind: "out";
297
312
  channel: string;
298
313
  text: string;
314
+ authentication?: boolean;
299
315
  at: number;
300
316
  replyTo?: string;
301
317
  buttons?: string[];