@primusclaw/personal-agent 0.4.0 → 0.6.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.
@@ -34,3 +34,10 @@ export interface Answers {
34
34
  * typed; a choice the card never offered is ignored.
35
35
  */
36
36
  export declare function answersOf(data: Readonly<Record<string, unknown>>): Answers;
37
+ /**
38
+ * A message the person typed in the chat while a question card waited, taken as the card's answer. With one question, a
39
+ * reply that names its options (by label or by number, several of them for a multiple choice) answers with their labels,
40
+ * as a press on the card would; any other reply is the free answer the card's text box takes. With several questions the
41
+ * whole reply answers each of them: the agent reads which part answers which. Nothing is skipped.
42
+ */
43
+ export declare function typedAnswers(questions: readonly CardQuestion[], typed: string): Answers;
@@ -35,8 +35,8 @@ export declare const TURN_DEADLINE_SECONDS: number;
35
35
  * the person the turn is for (Python `store.save_approval(sender.aad_id, ...)`).
36
36
  */
37
37
  export interface TurnApprovalRecords {
38
- record(userId: string, sessionId: string, toolUseId: string, tool: string, expiresIn: number | undefined, envelope: Envelope): boolean;
39
- sent(sessionId: string, toolUseId: string): void;
38
+ record(userId: string, sessionId: string, toolUseId: string, tool: string, expiresIn: number | undefined, envelope: Envelope, questions?: unknown): boolean;
39
+ sent(sessionId: string, toolUseId: string, cardId?: string): void;
40
40
  unsent(sessionId: string, toolUseId: string): void;
41
41
  }
42
42
  /** The records of the approvals a turn shows (the Python `on_approval`, `on_approval_sent`, `on_approval_unsent`). */
@@ -3,6 +3,7 @@
3
3
  * channel adapter renders them; `approvalView` (approval-view.ts) is what an approval card says.
4
4
  */
5
5
  import type { JsonObject } from "../json.js";
6
+ import type { Lang } from "../locales/lang.js";
6
7
  import type { CardQuestion } from "./questions.js";
7
8
  /** An approval card: the arguments of the Python worker's `render.permission_card`, one for one. */
8
9
  export interface ApprovalCardModel {
@@ -27,6 +28,11 @@ export interface QuestionCardModel {
27
28
  readonly ownerName: string;
28
29
  /** A line under the header: which task asks, or what happened when the card comes back after a click ("" when none). */
29
30
  readonly note: string;
31
+ /**
32
+ * The card says it can also be answered by a message in the chat: a turn's question, which a typed message answers
33
+ * (`routing/question-reply.ts`). A work item's question is answered on its card only.
34
+ */
35
+ readonly typedReply?: boolean;
30
36
  }
31
37
  /** What replaces an approval card once it was answered (render.decided_card). */
32
38
  export interface ApprovalDecidedModel {
@@ -47,6 +53,8 @@ export interface QuestionAnsweredModel {
47
53
  readonly answers: readonly (readonly [string, string])[];
48
54
  readonly when: number;
49
55
  readonly zone: string;
56
+ /** Answered by a message typed in the chat (not on the card), in this language: the card says "Answered: <answer>". */
57
+ readonly replied?: Lang;
50
58
  }
51
59
  /** An approval card put back after a press that did not go through: the note, then Approve and Deny again. */
52
60
  export interface ApprovalAgainModel {
@@ -89,6 +89,8 @@ export declare class AssistantChat {
89
89
  forget(userId: string): void;
90
90
  /** What a message of files and no words says to the assistant. */
91
91
  filesOnly(lang: Lang): string;
92
+ /** The language the person chose to be answered in, as the message's `channel_context` carries it (none: nothing is sent). */
93
+ private chosenLanguage;
92
94
  private langOf;
93
95
  /**
94
96
  * A message of a person the assistant understands, with the chat's context. True when the assistant has it; otherwise the
@@ -0,0 +1,35 @@
1
+ import type { ClickHandler, ClickRoute } from "../ports/routing.js";
2
+ import type { ChoiceCard } from "../ports/store/assistant.js";
3
+ import type { StorePort } from "../ports/store/index.js";
4
+ import type { Logger } from "../ports/logger.js";
5
+ import type { InboundMessage } from "../ports/surface.js";
6
+ export declare const CHOICE_VERB = "assistant_choice";
7
+ /** The id of a choice card's input, whose value is the picked options' places on the card ("0", "0,2"). */
8
+ export declare const CHOICE_INPUT = "choice";
9
+ export interface ChoiceClickDeps {
10
+ readonly store: Pick<StorePort, "assistantButtons">;
11
+ /** Handles `message` as the router handles one the person typed (`Router.handle`); throws when it was not handled. */
12
+ readonly deliver: (message: InboundMessage) => Promise<void>;
13
+ readonly logger: Logger;
14
+ }
15
+ /** The options `value` picks on `card` (each named by its place), in the card's order; a place it never had is no pick. */
16
+ export declare function pickedOn(card: Pick<ChoiceCard, "options">, value: unknown): string[];
17
+ /**
18
+ * The message a press sends: the presser's, in the chat it was pressed in, saying `text`, as a typed one is (to the bot, in a
19
+ * group), but marked `choice`, so no word of it is read as a command.
20
+ */
21
+ export declare function typedMessage(click: Pick<ClickRoute, "action" | "sender">, text: string, id: string): InboundMessage;
22
+ export declare class AssistantChoiceClicks implements ClickHandler {
23
+ private readonly deps;
24
+ readonly verbs: readonly ["assistant_choice"];
25
+ constructor(deps: ChoiceClickDeps);
26
+ /** A 1:1 chat: answered with no Claw key (the message it sends is checked as any typed one is). */
27
+ beforeCredential(click: ClickRoute): Promise<boolean>;
28
+ inGroup(click: ClickRoute): Promise<boolean>;
29
+ press(click: ClickRoute): Promise<void>;
30
+ /** The card back, open, with `note` under its options. */
31
+ private again;
32
+ /** The card replaced by what was picked and sent (said as a line too when `announce`). */
33
+ private sent;
34
+ private model;
35
+ }
@@ -41,6 +41,23 @@ export declare function confirmWriteOf(event: Event): ConfirmWrite | undefined;
41
41
  * line that is no command, else the defaults in the person's language (`lang`; without it, the language of `text`).
42
42
  */
43
43
  export declare function suggestedRepliesOf(event: Event, text: string, styleOn: boolean, lang?: Lang): SuggestedReply[];
44
+ /** The fewest options a choice card shows (Claw sends from 2). */
45
+ export declare const CHOICE_OPTIONS_MIN = 2;
46
+ /** The most options a choice card shows (Claw sends at most 10). */
47
+ export declare const CHOICE_OPTIONS_MAX = 10;
48
+ /** Code points of one option (Claw sends at most 80): one line beside a radio button or a check box. */
49
+ export declare const CHOICE_OPTION_MAX = 80;
50
+ /** A choice an `assistant.say` puts to the person: the options, and whether several may be picked. */
51
+ export interface SayChoice {
52
+ readonly options: readonly string[];
53
+ readonly multiple: boolean;
54
+ }
55
+ /**
56
+ * The choice an `assistant.say` puts to the person (`choices`); undefined when it carries none this worker draws. Read whole or not
57
+ * at all: an option that is not one plain line of text, or too many or too few of them, and there is no card (the words still name
58
+ * the options, for the person to type).
59
+ */
60
+ export declare function choiceOf(event: Event): SayChoice | undefined;
44
61
  export interface RemoteNotice {
45
62
  /** `resolved`: a question a terminal asked no longer waits here (answered there, or given up); `resolved` says which. */
46
63
  readonly kind: "done" | "needs_you" | "error" | "resolved";
@@ -1,6 +1,7 @@
1
1
  /** What the assembly wires of the assistant (one line each; the views and work-item text are exported from the package root). */
2
2
  export { AssistantActionClicks } from "./action-handler.js";
3
3
  export { AssistantActions } from "./actions.js";
4
+ export { AssistantChoiceClicks } from "./choice-clicks.js";
4
5
  export { Board } from "./board.js";
5
6
  export { AssistantChat as AssistantChatService } from "./chat.js";
6
7
  export type { AssistantChatOptions } from "./chat.js";
@@ -62,7 +62,7 @@ export declare class ReceiptConversation implements Conversation {
62
62
  update(id: MessageId, message: OutMessage, options?: UpdateOptions): Promise<void>;
63
63
  delete(id: MessageId): ReturnType<Conversation["delete"]>;
64
64
  outgoing(message: OutMessage): Envelope;
65
- deliver(envelope: Envelope): Promise<void>;
65
+ deliver(envelope: Envelope): Promise<MessageId | undefined>;
66
66
  openStream(replyTo: MessageId, status: string): ReturnType<Conversation["openStream"]>;
67
67
  /** What the assistant said beside the handoff, when no text message carried it: said on its own. */
68
68
  sayPostscript(): Promise<void>;
@@ -5,8 +5,9 @@
5
5
  * receipt already shows of it (`shown`). It is an AI answer: the AI label and the thumbs under it, a citation for each source it
6
6
  * cites as [n], and the replies to tap under the last of its messages when no card follows.
7
7
  * - When the event's item holds a result that waits, a card with Accept and Stop; when it holds a write for the person, a card
8
- * that lets it through or not; when it puts a command to the person, a card with Run and Cancel (each recorded first, so its
9
- * buttons mean something). A card holds the words only when they are short and not shown already.
8
+ * that lets it through or not; when it puts a command to the person, a card with Run and Cancel; when it puts a choice to the
9
+ * person, a card with the options and Send (each recorded first, so its buttons mean something). A card holds the words only
10
+ * when they are short and not shown already.
10
11
  * - Then a card for each file the item's worker delivered, with Claw's own download link.
11
12
  *
12
13
  * Every word of this worker's own is in the person's language, never in that of what was said.
@@ -51,10 +52,17 @@ export declare class SayPlanner {
51
52
  /** The replies to tap, under the last of what was said, when no card follows. */
52
53
  private withReplies;
53
54
  /**
54
- * The card with buttons the event asks for, recorded first: a result that waits, a write Claw holds, or a command put to the
55
- * person; undefined for none. `holdsWords`: the card shows the words, which are then not said apart.
55
+ * The card with buttons the event asks for, recorded first: a result that waits, a write Claw holds, a command or a choice put
56
+ * to the person; undefined for none. `holdsWords`: the card shows the words, which are then not said apart.
56
57
  */
57
58
  private card;
59
+ /** A write Claw holds for the person (Allow, Deny) or a command put to them (Run, Cancel), recorded first; undefined for none. */
60
+ private askCard;
61
+ /**
62
+ * A choice put to the person (`choices`): its options and Send, recorded first (what a press sends is read from the record).
63
+ * Its Send sends what was picked as the person's own message.
64
+ */
65
+ private choiceCard;
58
66
  /** What was said, as AI answers with thumbs, each with the citations its own words cite (when the whole text cites at all). */
59
67
  private said;
60
68
  /** A result that waits: Accept and Stop, and (a task whose result names a pull request) auto-reply on or off. */
@@ -68,9 +68,31 @@ export interface ConfirmWriteModel {
68
68
  readonly allowLabel: string;
69
69
  readonly lang: string;
70
70
  }
71
+ /** A choice the assistant put to the person: its options as radio buttons (or check boxes) and Send. */
72
+ export interface ChoiceCardModel {
73
+ /** The words, when the card holds them ("" when they were said apart). */
74
+ readonly text: string;
75
+ readonly cardId: string;
76
+ readonly options: readonly string[];
77
+ /** Several may be picked (check boxes); else one (radio buttons). */
78
+ readonly multiple: boolean;
79
+ /** One line under the options when the card comes back unanswered ("" none). */
80
+ readonly note: string;
81
+ /** "zh" or "en"; null to follow what was said. */
82
+ readonly lang: string | null;
83
+ }
84
+ /** What replaces a choice card once the person sent what they picked: what they sent, under the words it answered. */
85
+ export interface ChoiceChosenModel {
86
+ readonly text: string;
87
+ /** The options picked, in the card's order. */
88
+ readonly chosen: readonly string[];
89
+ readonly lang: string | null;
90
+ }
71
91
  declare module "../ports/surface.js" {
72
92
  interface ViewMap {
73
93
  actionCard: ActionCardModel;
94
+ choiceCard: ChoiceCardModel;
95
+ choiceChosen: ChoiceChosenModel;
74
96
  board: BoardModel;
75
97
  confirmWrite: ConfirmWriteModel;
76
98
  workCard: WorkCardModel;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The language a person chose to be answered in (`/lang zh|en`), as it goes to Claw with each of their messages, so that the
3
+ * model's answer is in it as well as the agent's own texts.
4
+ *
5
+ * Only a choice the person said counts: a language learnt from what they write (`learnLang`), and `/lang auto`, are no
6
+ * choice, nothing is sent, and Claw reads the language from the message as before. To the assistant the choice goes in the
7
+ * message's `channel_context` (`reply_language`); to a plain session (a group's turn, or a 1:1 chat without the assistant) as
8
+ * the line Claw's assistant uses for it, after the message (`replyLanguageLine`).
9
+ */
10
+ import type { Lang } from "../locales/lang.js";
11
+ /** What of a person's preferences says the choice (`Prefs.lang`, `Prefs.langSet`). */
12
+ export interface LangPrefs {
13
+ readonly lang: string;
14
+ readonly langSet: boolean;
15
+ }
16
+ /** The language the person chose, or undefined when they chose none. */
17
+ export declare function chosenLang(prefs: LangPrefs): Lang | undefined;
18
+ /** The choice as `channel_context` carries it (`{ reply_language: "zh" }`); empty when there is none. */
19
+ export declare function replyLanguageContext(prefs: LangPrefs): {
20
+ reply_language?: Lang;
21
+ };
22
+ /** The line after a message to a plain session that names the language to answer in: `[Reply language: English]`. */
23
+ export declare function replyLanguageLine(lang: Lang): string;
@@ -28,4 +28,15 @@ export declare const approvalText: import("./catalog.js").Catalog<{
28
28
  }) => string;
29
29
  questionAsker: string;
30
30
  send: string;
31
+ /** Under a question card's questions: a client that does not show the card can still answer. */
32
+ questionReply: string;
33
+ /** The same in a group chat, where only a message that @mentions the bot reaches it. */
34
+ questionReplyGroup: string;
35
+ /** A question card once a message typed in the chat answered it. */
36
+ questionReplied: (p: {
37
+ answer: string;
38
+ }) => string;
39
+ questionRepliedAt: (p: {
40
+ at: string;
41
+ }) => string;
31
42
  }>;
@@ -67,6 +67,21 @@ export declare const assistantCardText: import("./catalog.js").Catalog<{
67
67
  askedTitle: (p: {
68
68
  n: number;
69
69
  }) => string;
70
+ choiceTitle: string;
71
+ choiceTitleMany: string;
72
+ choiceSend: string;
73
+ choiceTyped: string;
74
+ choiceNone: string;
75
+ choiceSent: (p: {
76
+ answer: string;
77
+ }) => string;
78
+ choiceGone: string;
79
+ choiceSending: (p: {
80
+ answer: string;
81
+ }) => string;
82
+ choiceFailed: string;
83
+ /** How the options picked are joined into the person's message. */
84
+ choiceJoin: string;
70
85
  version: (p: {
71
86
  version: number;
72
87
  }) => string;
@@ -21,6 +21,8 @@ export declare const turnText: import("./catalog.js").Catalog<{
21
21
  thinking: string;
22
22
  /** A turn still running when its stream has to close: the answer follows as a message of its own. */
23
23
  stillWorking: string;
24
+ /** A turn whose stream is closed because a card now waits for the person's choice. */
25
+ awaitingChoice: string;
24
26
  interrupted: string;
25
27
  /** What a turn the person stopped ends with. */
26
28
  stopped: string;
@@ -25,12 +25,29 @@ export interface NewApproval {
25
25
  readonly expiresAt: number;
26
26
  /** The card as it will be published, kept until `approvalSent` so a worker that dies before the send leaves it to the next. */
27
27
  readonly envelope?: JsonObject | null;
28
+ /**
29
+ * A question card of a turn: its questions as the card's button carries them. Kept so that what the person types next in
30
+ * the chat can be taken as the answer (`pendingQuestion`).
31
+ */
32
+ readonly questions?: readonly JsonObject[] | null;
33
+ }
34
+ /** A question card still waiting for its answer, with what it takes to answer it from a typed message. */
35
+ export interface PendingQuestion {
36
+ readonly sessionId: string;
37
+ readonly toolUseId: string;
38
+ readonly userId: string;
39
+ /** When it runs out, epoch seconds. */
40
+ readonly expiresAt: number;
41
+ /** The questions as recorded with the card (`NewApproval.questions`). */
42
+ readonly questions: readonly JsonObject[];
43
+ /** The chat's id of the card ("" when the channel never said it: the card cannot be edited). */
44
+ readonly cardId: string;
28
45
  }
29
46
  export interface ApprovalRepository {
30
47
  /** True when this call recorded it; false when a card for the same approval was recorded before. */
31
48
  saveApproval(approval: NewApproval): boolean;
32
- /** The card reached the chat: nothing is left to send. */
33
- approvalSent(sessionId: string, toolUseId: string): void;
49
+ /** The card reached the chat: nothing is left to send. `cardId`: the chat's id of the card, when it said it. */
50
+ approvalSent(sessionId: string, toolUseId: string, cardId?: string): void;
34
51
  /** Its card is being sent right now by this process and may still fail. */
35
52
  approvalInFlight(sessionId: string, toolUseId: string): boolean;
36
53
  /** The card never reached the chat: take the record back (a row answered meanwhile stays). */
@@ -40,6 +57,11 @@ export interface ApprovalRepository {
40
57
  /** A claimed card could not be sent: the next sweep tries again. */
41
58
  approvalSendFailed(sessionId: string, toolUseId: string): void;
42
59
  getApproval(sessionId: string, toolUseId: string): Approval | null;
60
+ /**
61
+ * The newest question card of `userId` in `sessionId` that is still pending, in time, and was recorded with its questions;
62
+ * null when there is none.
63
+ */
64
+ pendingQuestion(sessionId: string, userId: string): PendingQuestion | null;
43
65
  /** Move an approval to `state`; true if this call did it (only from 'pending' unless `onlyIfPending` is false). */
44
66
  closeApproval(sessionId: string, toolUseId: string, state: ApprovalState, options?: {
45
67
  readonly onlyIfPending?: boolean;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Cards and messages of the assistant (tables `assistant_cards`, `auto_replies`, `assistant_action_cards`,
3
- * `assistant_confirm_cards`, `assistant_messages`). A card's buttons carry only its id; what a press means is read
3
+ * `assistant_confirm_cards`, `assistant_choice_cards`, `assistant_messages`). A card's buttons carry only its id; what a press means is read
4
4
  * from here, so a forged, old or someone else's card changes nothing.
5
5
  */
6
6
  import type { JsonObject } from "./json.js";
@@ -56,6 +56,21 @@ export interface ConfirmCard {
56
56
  readonly state: CardState;
57
57
  readonly createdAt: number;
58
58
  }
59
+ /** A choice the assistant put to the person: what was asked, the options, and (once sent) what was picked. */
60
+ export interface ChoiceCard {
61
+ readonly cardId: string;
62
+ readonly userId: string;
63
+ /** The words the card holds ("" when they were said apart). */
64
+ readonly text: string;
65
+ readonly options: readonly string[];
66
+ readonly multiple: boolean;
67
+ /** What was picked, in the card's order: being sent ('busy') or sent ('done'); empty before the first press. */
68
+ readonly chosen: readonly string[];
69
+ /** 'open'; 'busy' while a press sends it; 'done' once the router took it. */
70
+ readonly state: CardState;
71
+ readonly lang: string;
72
+ readonly createdAt: number;
73
+ }
59
74
  export interface AssistantMessage {
60
75
  readonly userId: string;
61
76
  /** The actions (and handoffs) the message declared. */
@@ -94,6 +109,17 @@ export interface AssistantButtonRepository {
94
109
  /** Null once it is older than CONFIRM_CARD_KEEP_SECONDS. */
95
110
  getConfirmCard(cardId: string): ConfirmCard | null;
96
111
  moveConfirmCard(cardId: string, state: CardState, options?: MoveOptions): boolean;
112
+ addChoiceCard(userId: string, choice: {
113
+ readonly text: string;
114
+ readonly options: readonly string[];
115
+ readonly multiple: boolean;
116
+ readonly lang: string;
117
+ }): string;
118
+ /** Null once it is older than ASSISTANT_MESSAGE_KEEP_SECONDS. */
119
+ getChoiceCard(cardId: string): ChoiceCard | null;
120
+ /** An open card is claimed by a press to send `chosen` (it moves to 'busy'); true if this call claimed it, so of two presses exactly one sends. */
121
+ claimChoiceCard(cardId: string, chosen: readonly string[]): boolean;
122
+ moveChoiceCard(cardId: string, state: CardState, options?: MoveOptions): boolean;
97
123
  }
98
124
  export interface AssistantMessageRepository {
99
125
  /** A message this worker sent to the person's assistant, with the actions it declared and the Claw users it named. */
@@ -134,6 +134,12 @@ export interface InboundMessage extends InboundBase {
134
134
  */
135
135
  readonly unreadableFiles: readonly string[];
136
136
  readonly provenance: Provenance;
137
+ /**
138
+ * What was picked on a choice card the assistant put (`assistant_choice`): an answer, never read as a command. The menu's
139
+ * words and help's phrases ("New", "重新开始", "Stop", "关闭记忆") stay the words picked; it goes where a typed message that
140
+ * is no command goes (the assistant, or the agent a 1:1 chat is attached to).
141
+ */
142
+ readonly choice?: true;
137
143
  }
138
144
  /** A button pressed on a card the bot posted. */
139
145
  export interface InboundAction extends InboundBase {
@@ -336,8 +342,11 @@ export interface Conversation {
336
342
  delete(id: MessageId): Promise<Result<void, DeleteUnsupported>>;
337
343
  /** The envelope `send` would publish for `message` (masked in a group), built without sending it. */
338
344
  outgoing(message: OutMessage): Envelope;
339
- /** Publish an envelope `outgoing` built (possibly in another process, from storage). */
340
- deliver(envelope: Envelope): Promise<void>;
345
+ /**
346
+ * Publish an envelope `outgoing` built (possibly in another process, from storage). The id of the posted message when the
347
+ * channel said it at once (a direct send); undefined otherwise (a queued send: the message cannot be edited by id).
348
+ */
349
+ deliver(envelope: Envelope): Promise<MessageId | undefined>;
341
350
  /**
342
351
  * A live stream that answers `replyTo` (the person's message), showing `status` as its first informative update.
343
352
  * Success means that first update has been sent and the channel took it; refused when the channel cannot stream here
@@ -140,6 +140,11 @@ export interface AssistantRemoteAsk {
140
140
  question_index?: number;
141
141
  question_count?: number;
142
142
  }
143
+ /** A choice a say puts to the person: from 2 to 10 options, one plain line each; `multiple` when several may be picked. */
144
+ export interface AssistantSayChoice {
145
+ options: string[];
146
+ multiple: boolean;
147
+ }
143
148
  /** Something the assistant chose to say to the user. */
144
149
  export interface AssistantSayPayload extends AssistantEventBase {
145
150
  type: "assistant.say";
@@ -151,6 +156,12 @@ export interface AssistantSayPayload extends AssistantEventBase {
151
156
  * next, each one plain line. A channel shows them as buttons that send the words as the user's message; one that cannot ignores them.
152
157
  */
153
158
  suggested_replies?: string[];
159
+ /**
160
+ * Present only when the router put a choice to the person (`CLAW_ASSISTANT_CHOICES`): the options they pick from, each one plain
161
+ * line, and whether they may pick several. A channel shows them as a card whose Send sends the picked options as the person's own
162
+ * message (their words, joined); one that cannot ignores it, and the text still names the options.
163
+ */
164
+ choices?: AssistantSayChoice;
154
165
  confirm_write?: AssistantConfirmWrite;
155
166
  /** Files the user gets with the message (a worker's deliveries, `deliveries.ts`): one download each. */
156
167
  files?: AssistantFile[];
@@ -1,4 +1,6 @@
1
- import type { OutMessage } from "../ports/surface.js";
1
+ import type { LinkRepository } from "../ports/store/link.js";
2
+ import type { OutMessage, UserRef } from "../ports/surface.js";
3
+ import type { TokenExchange } from "../plugins/token-exchange.js";
2
4
  import type { Chat } from "./chat.js";
3
5
  import type { RouterDeps } from "./deps.js";
4
6
  import type { CredentialProvider } from "./features.js";
@@ -12,6 +14,12 @@ export declare const IDENTITY_CARD_INTERVAL_SECONDS: number;
12
14
  /** Its kind in the store's notice record (`claimGithubNotice`): the time it was last sent to the person. */
13
15
  export declare const IDENTITY_CARD_NOTICE = "identity_card";
14
16
  type CredentialDeps = Pick<RouterDeps, "store" | "exchange" | "config" | "logger">;
17
+ /**
18
+ * What an action of the person's outside a turn (a thumb on an answer) calls Claw with: what their chats run on, so it acts
19
+ * on the same session as the same Claw user. With chats on the exchange (`exchange` given), the person's exchanged token,
20
+ * else their linked key; without, the linked key. Undefined when they have neither. Never asks or tells them anything.
21
+ */
22
+ export declare function chatCredentialOf(links: Pick<LinkRepository, "getKey">, exchange: TokenExchange | undefined): (sender: UserRef) => Promise<string | undefined>;
15
23
  /**
16
24
  * What Claw is called with for a person, and what a person who has none is told (the Python `Agent._credential`,
17
25
  * `_unlinked`, `_connect_identity`, `_binding_card`, `_ask_to_link`, `_background_credential`, `_fresh_credential`).
@@ -38,7 +38,7 @@ export interface RouterConfig {
38
38
  readonly ownThumbsMobile: boolean;
39
39
  }
40
40
  /** The repositories the router reads and writes (the W1 store, as far as routing goes). */
41
- export type RouterStore = Pick<StorePort, "links" | "references" | "prefs" | "members" | "sessions" | "repoHistory" | "turns" | "onboarding" | "idempotency" | "assistantMessages" | "notifySubs" | "reviewDelegation" | "now">;
41
+ export type RouterStore = Pick<StorePort, "links" | "references" | "prefs" | "members" | "sessions" | "repoHistory" | "turns" | "onboarding" | "idempotency" | "assistantMessages" | "notifySubs" | "reviewDelegation" | "approvals" | "now">;
42
42
  /** The per-person limits (Python `limits.py`): messages per window and tasks at the same time. */
43
43
  export interface RouterLimits {
44
44
  readonly maxConcurrentTasks: number;
@@ -2,7 +2,7 @@ import type { InboundFile, InboundMessage } from "../ports/surface.js";
2
2
  import type { Chat } from "./chat.js";
3
3
  import type { CommandTable } from "./command-table.js";
4
4
  import type { RouterContext } from "./context.js";
5
- import type { TurnStarter } from "./turn-start.js";
5
+ import { type TurnStarter } from "./turn-start.js";
6
6
  /**
7
7
  * A 1:1 message that is not one of the chat's commands, as it goes on (the Python `Agent._text_of`, `_on_text`,
8
8
  * `_sent_on`, `_talks_to_agent`, `_session_control`, `_understand`, `_understood`, `_offer_github`, `_attachments`):
@@ -15,7 +15,8 @@ export declare class PersonalText {
15
15
  constructor(ctx: RouterContext, commands: CommandTable, starter: TurnStarter);
16
16
  /**
17
17
  * What the person wrote, with a word of the commands menu read as its command and a sentence help teaches read as the
18
- * command it means; a sentence for what this chat cannot do right now stays words.
18
+ * command it means; a sentence for what this chat cannot do right now stays words. A choice picked on the assistant's card
19
+ * is the words picked, never a command.
19
20
  */
20
21
  textOf(message: InboundMessage, group?: boolean): string;
21
22
  onText(message: InboundMessage, chat: Chat, key: string): Promise<void>;
@@ -0,0 +1,22 @@
1
+ import type { Chat } from "./chat.js";
2
+ import type { RouterContext } from "./context.js";
3
+ /**
4
+ * A question with less than this left of its wait is not answered from a typed message: Brain may already have stopped
5
+ * waiting (the card's time is counted from when the worker saw the question, a little after Brain's), and an answer nobody
6
+ * waits for is dropped. The message then goes on as a new one, as before.
7
+ */
8
+ export declare const TYPED_ANSWER_MARGIN_SECONDS = 15;
9
+ /** A typed message that may answer the person's waiting question. */
10
+ export interface TypedReply {
11
+ readonly chat: Chat;
12
+ readonly text: string;
13
+ /** The Claw credential of the person, read when there is a question to answer. */
14
+ readonly credential: () => Promise<string>;
15
+ }
16
+ /**
17
+ * Answers the question card that waits in the chat's session with `reply.text`: true when it did (the card now says
18
+ * "Answered: ...", and the message must not go on as a new one); false when there was nothing to answer, or it could not be
19
+ * answered (the message then goes on as before). Only the person the card was raised for answers it: their own session in
20
+ * this chat, their own record.
21
+ */
22
+ export declare function answerWaitingQuestion(ctx: RouterContext, reply: TypedReply): Promise<boolean>;
@@ -43,6 +43,11 @@ export interface TurnRun {
43
43
  readonly sent: string;
44
44
  readonly files: readonly InboundFile[];
45
45
  readonly lang: Lang;
46
+ /**
47
+ * The language the person chose to be answered in (`/lang zh|en`, `chosenLang`), told to Claw after the message; undefined: they
48
+ * chose none, and the answer is in the language of what they wrote. A chat turn's only: a group's is the sender's.
49
+ */
50
+ readonly replyLang?: Lang;
46
51
  /** The answer gets thumbs of the worker's own (a phone, whose own thumbs never reach the worker). */
47
52
  readonly ownThumbs: boolean;
48
53
  /** Claw pushes this person a run's end and its approvals. */
@@ -1,10 +1,22 @@
1
1
  import type { Lang } from "../locales/lang.js";
2
+ import type { Logger } from "../ports/logger.js";
2
3
  import type { InboundFile } from "../ports/surface.js";
3
4
  import type { Chat } from "./chat.js";
4
5
  import type { RouterContext } from "./context.js";
5
6
  import type { ReviewRunFields, TurnOutcome } from "./turn-runner.js";
6
7
  /** Teams on a phone or a tablet: its own Stop under a reply is greyed out, and its own thumbs never reach the worker. */
7
8
  export declare function isMobile(clientPlatform: string): boolean;
9
+ /**
10
+ * The path a message from Teams took: to the person's assistant, to a plain Claw session of their own (the old path, neither
11
+ * the assistant's nor a work item's), or to a review's turn. Logged so the worker's lines can be counted per path.
12
+ */
13
+ export type TurnRoute = "assistant" | "legacy" | "review";
14
+ /**
15
+ * The line for a turn started from Teams: where it came from and which path it took, never its words or any id. One line
16
+ * per turn: a message to the assistant or to a plain session is one turn, and a review request for several pull requests
17
+ * starts one review turn (and one line) for each.
18
+ */
19
+ export declare function logTurnStarted(logger: Logger, clientPlatform: string, group: boolean, route: TurnRoute): void;
8
20
  /** A review the review service starts as a turn here (its item runs in Claw already; the turn only follows it). */
9
21
  export interface ReviewTurnStart {
10
22
  /** The work item Claw made for it ("" none: the turn makes a session of its own). */
@@ -41,6 +53,11 @@ export interface StartTurn {
41
53
  readonly sent?: string;
42
54
  readonly files?: readonly InboundFile[];
43
55
  readonly review?: ReviewTurnStart;
56
+ /**
57
+ * The person typed it in the chat (not a button, not a review): while the agent waits on a question card in this chat's
58
+ * session, it is the answer to that question and starts no turn.
59
+ */
60
+ readonly typed?: boolean;
44
61
  }
45
62
  /**
46
63
  * Starting and following a turn (the Python `Agent._start_turn`, `_queue_behind`, `_guarded`, `_record_session`,
@@ -55,7 +72,9 @@ export declare class TurnStarter {
55
72
  /** The turns that run now (a test waits for them). */
56
73
  settled(): Promise<void>;
57
74
  /** The turn that runs (its end is `done`), or undefined when none was started (the person was told why). */
58
- start(request: StartTurn): Promise<RunningTurn | undefined>;
75
+ start(started: StartTurn): Promise<RunningTurn | undefined>;
76
+ /** A typed message the waiting question card of the chat's session took as its answer (`question-reply.ts`). */
77
+ private answersQuestion;
59
78
  private track;
60
79
  /** The turn goes to Claw now; its end is `done`. */
61
80
  private launch;
@@ -78,6 +97,8 @@ export declare class TurnStarter {
78
97
  /** What the run reads of the chat's session: its id, the model it has, the repository a new one is bound to. */
79
98
  private sessionPart;
80
99
  private runOf;
100
+ /** The language the sender chose to be answered in, for a chat turn (a review's answer follows its request's language). */
101
+ private replyLang;
81
102
  private hooks;
82
103
  private guarded;
83
104
  private tell;
@@ -1,4 +1,3 @@
1
- /** Everything one turn runs with: who it acts for, what it sends, where it answers, and what it reports back. */
2
1
  import type { Lang } from "../../locales/lang.js";
3
2
  import type { PullRef } from "../../claw/model.js";
4
3
  import type { Integrations } from "../../plugins/integrations.js";
@@ -60,6 +59,8 @@ export interface TurnRequest {
60
59
  readonly sent?: string;
61
60
  /** The language the person is answered in (undefined: the language of `text`). */
62
61
  readonly lang?: Lang;
62
+ /** The language the person chose to be answered in, told to Claw after the message (`replyLanguageLine`); undefined: none chosen. */
63
+ readonly replyLang?: Lang;
63
64
  /** What the user sent with the message. */
64
65
  readonly files?: readonly InboundFile[];
65
66
  }
@@ -70,9 +71,11 @@ export interface TurnHooks {
70
71
  /**
71
72
  * An approval or question is about to be shown, as `envelope`: false when a card for it is already in a chat (the
72
73
  * notification got there first), and the turn then shows none. `expiresIn`: the seconds Brain waits for the answer.
74
+ * `questions`: a question's questions, as its event carried them (undefined for an approval).
73
75
  */
74
- readonly onApproval?: (sessionId: string, toolUseId: string, tool: string, expiresIn: number | undefined, envelope: Envelope) => boolean | undefined;
75
- readonly onApprovalSent?: (sessionId: string, toolUseId: string) => void;
76
+ readonly onApproval?: (sessionId: string, toolUseId: string, tool: string, expiresIn: number | undefined, envelope: Envelope, questions?: unknown) => boolean | undefined;
77
+ /** The card is in the chat; `cardId`: the chat's id of it, when the chat said it. */
78
+ readonly onApprovalSent?: (sessionId: string, toolUseId: string, cardId?: string) => void;
76
79
  readonly onApprovalUnsent?: (sessionId: string, toolUseId: string) => void;
77
80
  /** (session, "started" | "delivered" | "failed", Claw message id or ''). */
78
81
  readonly onTurn?: (sessionId: string, what: "started" | "delivered" | "failed", messageId: string) => void;
@@ -152,6 +155,7 @@ export declare function credential(ctx: TurnContext): Promise<string>;
152
155
  export declare function langOf(ctx: TurnContext): Lang;
153
156
  /**
154
157
  * What Claw is sent: the quoted messages, then what the user wrote (with the addresses behind its links), then where the
155
- * files they sent are. `text` stays the words alone: the session's title, the language of the answer.
158
+ * files they sent are, then the language they chose to be answered in (`replyLanguageLine`), when they chose one. `text` stays the
159
+ * words alone: the session's title, the language of the answer.
156
160
  */
157
161
  export declare function messageOf(ctx: TurnContext): string;
@@ -18,5 +18,7 @@ export interface TurnControls {
18
18
  /** The turn is cancelled: no live edit is sent after it, and the run's record is still kept. */
19
19
  readonly signal?: AbortSignal;
20
20
  }
21
+ /** The id Claw gives a message it queued behind a running turn (`queuedMessageId`, claw/packages/api/src/tasks/chat-run.ts). */
22
+ export declare const QUEUED_MESSAGE_PREFIX = "claw-pending-";
21
23
  /** Runs the turn and always leaves the user a message; `controls.onSession` records the session id. */
22
24
  export declare function runTurn(ctx: TurnContext, controls: TurnControls): Promise<void>;