@primusclaw/personal-agent 0.3.0 → 0.5.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 {
@@ -54,6 +54,8 @@ export declare class Receipt {
54
54
  behind: string | undefined;
55
55
  unsent: boolean;
56
56
  private want;
57
+ /** Words of the reply were given to be shown (they may still be on their way to the channel). */
58
+ private wordsShown;
57
59
  private sender;
58
60
  private readonly closedWaiters;
59
61
  private readonly closer;
@@ -74,6 +76,8 @@ export declare class Receipt {
74
76
  * neither its turn nor the one in front of it, closes it as before.
75
77
  */
76
78
  answeredBy(turn: string | undefined): boolean;
79
+ /** Words of the reply are shown in it, or on their way to it: its turn has begun to answer. */
80
+ get answering(): boolean;
77
81
  /** Resolves when the receipt is closed (or being closed): what follows the turn stops then. */
78
82
  closed(): Promise<void>;
79
83
  /** Show `line` as the status, unless words of the reply are shown already. */
@@ -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>;
@@ -1,4 +1,5 @@
1
1
  import { type Lang } from "../locales/lang.js";
2
+ import { type GroupContextKept } from "../locales/transparency.js";
2
3
  import { type Part } from "./registry.js";
3
4
  export declare const CURRENT_WORDS: Readonly<Record<Lang, string>>;
4
5
  /** A confirmation setting as help says it: [Chinese, English]. */
@@ -19,6 +20,8 @@ export interface HelpOptions {
19
20
  readonly lead?: string;
20
21
  readonly bot?: string;
21
22
  readonly understanding?: boolean;
23
+ /** The deployment keeps a group's recent messages as background (what the data statement then says it sees). */
24
+ readonly groupContext?: GroupContextKept;
22
25
  }
23
26
  /** The help for a chat, in `language` ("zh", or anything else for English). */
24
27
  export declare function renderHelp(language: string, options?: HelpOptions): string;
@@ -38,7 +38,7 @@ export interface Settings {
38
38
  /** TEAMS_OBSERVE_USERS (lower-cased): who may turn on observing a group chat. */
39
39
  readonly observeUsers: ReadonlySet<string>;
40
40
  /**
41
- * TEAMS_GROUP_CONTEXT_MESSAGES (30; 0 off), TEAMS_GROUP_CONTEXT_HOURS (4), TEAMS_GROUP_CONTEXT_CHARS (500): the recent
41
+ * TEAMS_GROUP_CONTEXT_MESSAGES (30; 0 off), TEAMS_GROUP_CONTEXT_HOURS (4), TEAMS_GROUP_CONTEXT_CHARS (500), TEAMS_GROUP_CONTEXT_GRANTED (off): the recent
42
42
  * messages of a group chat kept in memory as background for an @mention.
43
43
  */
44
44
  readonly groupContext: GroupContextSettings;
@@ -10,6 +10,7 @@
10
10
  * @mentions the bot, `background` gives what is kept, marked as untrusted history, for the turn to send before what they
11
11
  * wrote.
12
12
  */
13
+ import type { GroupContextKept } from "../locales/transparency.js";
13
14
  import type { Clock } from "../ports/clock.js";
14
15
  import type { Logger } from "../ports/logger.js";
15
16
  import type { GroupContextRepository } from "../ports/store/group-context.js";
@@ -22,6 +23,12 @@ export interface GroupContextSettings {
22
23
  readonly maxAgeSeconds: number;
23
24
  /** The longest text kept of one message (code points). */
24
25
  readonly messageChars: number;
26
+ /**
27
+ * The deployment's chat app asks for the permission that delivers a group's other messages (Teams:
28
+ * ChatMessage.Read.Chat), so help says what is kept. Without it no such message arrives and help keeps the words
29
+ * "only the @mentions".
30
+ */
31
+ readonly granted: boolean;
25
32
  }
26
33
  export declare const DEFAULT_GROUP_CONTEXT: GroupContextSettings;
27
34
  /** The most chats kept at once: past it, the chat that heard nothing for longest is dropped. */
@@ -40,6 +47,11 @@ export declare class GroupContext {
40
47
  private readonly chats;
41
48
  constructor(deps: GroupContextDeps);
42
49
  get enabled(): boolean;
50
+ /**
51
+ * How much a chat keeps, as help says it: undefined when the deployment keeps nothing, its app is not granted the
52
+ * group's other messages (so none arrives), or `chatId` turned it off.
53
+ */
54
+ kept(chatId?: string): GroupContextKept | undefined;
43
55
  /** A group message not addressed to the bot: kept, never answered. */
44
56
  record(message: InboundMessage): void;
45
57
  /** The person edited a kept message: the new words replace the old (nothing when it is not kept). */
@@ -9,6 +9,11 @@ export declare const accountText: import("./catalog.js").Catalog<{
9
9
  email: string;
10
10
  }) => string;
11
11
  whoamiUnknown: string;
12
+ whoamiBound: (p: {
13
+ name: string;
14
+ email: string;
15
+ }) => string;
16
+ whoamiBoundUnknown: string;
12
17
  memoryUnreadable: (p: {
13
18
  page: string;
14
19
  }) => 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
  }>;
@@ -14,5 +14,26 @@ export declare const DATA_TITLE: Phrase;
14
14
  export declare const DATA_LINES: Readonly<Record<Lang, readonly string[]>>;
15
15
  /** Only where remote agents are on (a 1:1 chat): an attached chat's messages go to the person's own machine, not to PrimusClaw. */
16
16
  export declare const REMOTE_LINE: Phrase;
17
- /** The section of help about answers and data, in `lang`; `remote`: remote agents are on in this chat. */
18
- export declare function dataStatement(lang: string, remote?: boolean): string;
17
+ /**
18
+ * How much of a group chat's recent messages the deployment keeps as background for an @mention (core's
19
+ * `GroupContext`, TEAMS_GROUP_CONTEXT_*), when that is on: the newest `messages`, none older than `hours`.
20
+ */
21
+ export interface GroupContextKept {
22
+ readonly messages: number;
23
+ readonly hours: number;
24
+ }
25
+ /** "What I see" where a group's recent messages are kept: the group's @mentions are no longer all it sees. */
26
+ export declare const SEES_LINE: Phrase;
27
+ /**
28
+ * Only where the deployment keeps a group's recent messages (#725) and its app is granted them: what is kept, for how
29
+ * long, where it goes, and how to stop it; one short line each (help keeps every line short enough for a phone). The
30
+ * worker's buffer is memory only, but what it holds goes into the asker's session with an @mention (as the turn's
31
+ * quote), and a session is kept as the statement's "how long" says: so "not stored on their own", never "never stored".
32
+ * "May": a group whose mentions go to a remote agent (group-remote.ts) does not send them along.
33
+ */
34
+ export declare function groupContextLines(lang: Lang, kept: GroupContextKept): readonly string[];
35
+ /**
36
+ * The section of help about answers and data, in `lang`; `remote`: remote agents are on in this chat; `groupContext`:
37
+ * the deployment keeps a group's recent messages (absent: it does not, and a group's @mentions are all it sees).
38
+ */
39
+ export declare function dataStatement(lang: string, remote?: boolean, groupContext?: GroupContextKept): string;
@@ -21,7 +21,11 @@ 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;
27
+ /** What a turn the person stopped ends with. */
28
+ stopped: string;
25
29
  /** The final message of a stream whose turn ended with nothing to say (it is never empty). */
26
30
  doneShort: string;
27
31
  /** What a failed turn says; the error itself, and its reference, go to the log only. */
@@ -46,7 +50,6 @@ export declare const turnEnglish: {
46
50
  readonly onlyTheAsker: " (only the person who asked can open it)";
47
51
  readonly wholeCutNote: "\n\n… (the rest did not fit in three messages)";
48
52
  readonly couldNotLoad: (name: string, count: number) => string;
49
- readonly stopped: "Stopped.";
50
53
  readonly lostConnection: "I lost the connection to PrimusClaw before the task finished.";
51
54
  readonly openSessionLink: "Open the session";
52
55
  readonly keyRejected: "PrimusClaw rejected your linked key (it may have expired or been deleted). Send /unlink, then link again.";
@@ -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;
@@ -336,8 +336,11 @@ export interface Conversation {
336
336
  delete(id: MessageId): Promise<Result<void, DeleteUnsupported>>;
337
337
  /** The envelope `send` would publish for `message` (masked in a group), built without sending it. */
338
338
  outgoing(message: OutMessage): Envelope;
339
- /** Publish an envelope `outgoing` built (possibly in another process, from storage). */
340
- deliver(envelope: Envelope): Promise<void>;
339
+ /**
340
+ * Publish an envelope `outgoing` built (possibly in another process, from storage). The id of the posted message when the
341
+ * channel said it at once (a direct send); undefined otherwise (a queued send: the message cannot be edited by id).
342
+ */
343
+ deliver(envelope: Envelope): Promise<MessageId | undefined>;
341
344
  /**
342
345
  * A live stream that answers `replyTo` (the person's message), showing `status` as its first informative update.
343
346
  * Success means that first update has been sent and the channel took it; refused when the channel cannot stream here
@@ -4,9 +4,13 @@ import type { ProactiveDeps } from "./deps.js";
4
4
  import { type AnsweredInChat } from "./events.js";
5
5
  /** What the person asked, as the name of a run in its card and in the line before a lost turn's answer. */
6
6
  export declare const PROMPT_TITLE_MAX = 60;
7
+ /** How many turns this process remembers that a say of theirs was delivered for (`Deliveries.answered`). */
8
+ export declare const ANSWERED_TURNS_MAX = 512;
7
9
  export declare class Deliveries {
8
10
  private readonly deps;
9
11
  private readonly answeredInChat;
12
+ /** Turns a say of which (other than Claw's "on it" line) this process began to deliver, newest last. */
13
+ private readonly answered;
10
14
  constructor(deps: ProactiveDeps, answeredInChat: (event: JsonObject, now: number) => AnsweredInChat);
11
15
  /** Send the message for `event`; false when nothing was sent (an approval already in a chat, an event that says nothing). */
12
16
  send(userId: string, reference: JsonObject, event: JsonObject): Promise<boolean>;
@@ -23,6 +27,15 @@ export declare class Deliveries {
23
27
  * makes the message undelivered.
24
28
  */
25
29
  private sendSaid;
30
+ /**
31
+ * Claw's "on it" line for a turn that has already begun to answer: a say of the turn was delivered (or is being), or the
32
+ * receipt under the person's message is showing the reply's words. The line came late (it was decided before the answer,
33
+ * or the two crossed on the way here): it is dropped, never put in the receipt's place, where it would replace the words of
34
+ * a reply that is being written or close it as if it were the answer (2026-11-11 0.3.0 acceptance: a refusal streamed into
35
+ * the receipt, the line closed it as "收到,我看一下。", and the refusal came as a message of its own).
36
+ */
37
+ private lateTurnAck;
38
+ private noteAnswered;
26
39
  /** What the assistant said (or a work item that needs the person and was not said). */
27
40
  private sendSaidWords;
28
41
  /** The client locale of the message being handled ("" for a push or a sweep: Python CLIENT_LOCALE's default). */
@@ -76,8 +76,9 @@ export interface ProactiveDeps {
76
76
  readonly requestScope?: RequestScope;
77
77
  /**
78
78
  * After an assistant message whose turn was refused for want of a confirmed identity (`needs_identity`), in the person's own
79
- * chat: the card that confirms it (`Credentials.identityAfterRefusal`), once per turn (`turn`: the message the turn answers,
80
- * else the say's own id; a later say of the same turn replaces the first and carries the field too). Default: none.
79
+ * chat: the card that confirms it (`Credentials.identityAfterRefusal`: at most once per person in 24 hours, none once they
80
+ * confirmed), and never twice for one turn (`turn`: the message the turn answers, else the say's own id; a later say of the
81
+ * same turn replaces the first and carries the field too). Default: none.
81
82
  */
82
83
  readonly identityAfterRefusal?: (userId: string, conversation: Conversation, turn: string) => Promise<void>;
83
84
  }
@@ -1,4 +1,3 @@
1
- /** What the delivery of unasked messages adds to Claw's events, and reads of them, shared by its modules. */
2
1
  import type { JsonObject } from "../json.js";
3
2
  /** Epoch seconds, added by this worker to a held approval event. */
4
3
  export declare const EXPIRES_KEY = "_expires_at";
@@ -8,3 +7,12 @@ export declare const REMOTE_SAID = "remote.said";
8
7
  export declare function finishedAt(event: JsonObject, now: number): number;
9
8
  /** Whether this worker's own turn answered the run in the chat ("lost": a turn of an earlier process died answering it). */
10
9
  export type AnsweredInChat = "delivered" | "running" | "lost" | null;
10
+ /**
11
+ * The id Claw gives its own "on it" line for the turn answering the person's message `messageId` ("收到,我看一下。", said by the
12
+ * server when the turn has said nothing by a deadline or starts a slow lookup): Claw's `lookupAckSayId`, a deterministic event id
13
+ * (`deterministicEventId("assistant.notice", <Claw user id>, "lookup_ack:<message id>")`, sha256 shaped as a uuid). The line carries
14
+ * no field of its own that says what it is; its id does.
15
+ */
16
+ export declare function turnAckId(clawUserId: string, messageId: string): string;
17
+ /** The turn whose "on it" line `event` is (`turnAckId`), or "" when it is not one. */
18
+ export declare function turnAckOf(event: JsonObject): string;
@@ -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";
@@ -7,7 +9,17 @@ import type { CommandViews } from "./views.js";
7
9
  export declare class CredentialUnavailable extends Error {
8
10
  readonly name = "CredentialUnavailable";
9
11
  }
12
+ /** The identity card after a refused answer goes to a person at most this often (`identityAfterRefusal`). */
13
+ export declare const IDENTITY_CARD_INTERVAL_SECONDS: number;
14
+ /** Its kind in the store's notice record (`claimGithubNotice`): the time it was last sent to the person. */
15
+ export declare const IDENTITY_CARD_NOTICE = "identity_card";
10
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>;
11
23
  /**
12
24
  * What Claw is called with for a person, and what a person who has none is told (the Python `Agent._credential`,
13
25
  * `_unlinked`, `_connect_identity`, `_binding_card`, `_ask_to_link`, `_background_credential`, `_fresh_credential`).
@@ -28,8 +40,10 @@ export declare class Credentials {
28
40
  /**
29
41
  * After an answer of the assistant that was refused for want of a confirmed identity (`needs_identity`: a person whose chats
30
42
  * run on their key because they never confirmed it asked for something only they may do, such as a review as them): the card
31
- * that confirms it, unless the person has confirmed by now. Nothing when chats do not run on the exchange, or it cannot be
32
- * asked. Never before that: connecting a machine and the rest of what a key may do are not held behind it.
43
+ * that confirms it, at most once per person in IDENTITY_CARD_INTERVAL_SECONDS (the time it was last sent is kept in the
44
+ * store, so a restart or another replica does not send it again), and never once the person has confirmed. Nothing when
45
+ * chats do not run on the exchange, or it cannot be asked; a card that was not sent leaves the allowance unspent. Never
46
+ * before such an answer: connecting a machine and the rest of what a key may do are not held behind it.
33
47
  */
34
48
  identityAfterRefusal(chat: Chat): Promise<boolean>;
35
49
  /** The binding card with `lead`; false (nothing sent) when the identity service gives no link: a key's owner is not sent the `/link` steps. */
@@ -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" | "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,6 +2,7 @@ import type { GithubConnection } from "../claw/model.js";
2
2
  import type { JsonObject } from "../json.js";
3
3
  import type { KeyOwner } from "../plugins/identity-lookup.js";
4
4
  import type { Lang } from "../locales/lang.js";
5
+ import type { GroupContextKept } from "../locales/transparency.js";
5
6
  import type { Conversation, InboundFile, InboundMessage, OutMessage, UserRef } from "../ports/surface.js";
6
7
  /**
7
8
  * What the router asks of the features it hands messages to. Each is implemented by the feature's own module (the
@@ -156,6 +157,8 @@ export interface GroupHistory {
156
157
  record(message: InboundMessage): void;
157
158
  /** The chat's kept messages as background for a turn ("" none). */
158
159
  background(chatId: string): string;
160
+ /** How much a chat keeps, for help to say (undefined: nothing arrives or is kept, or `chatId` turned it off). */
161
+ kept(chatId?: string): GroupContextKept | undefined;
159
162
  }
160
163
  /** The review service's part in the chat (Python `github_review.GithubReview` and `github_connect.GithubConnect`). */
161
164
  export interface GithubChat {
@@ -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`):
@@ -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>;
@@ -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`,
@@ -56,6 +73,8 @@ export declare class TurnStarter {
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
75
  start(request: 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;
@@ -21,6 +21,8 @@ export interface BindingCardView {
21
21
  /** A first sentence ("" none). */
22
22
  readonly lead: string;
23
23
  readonly notice: string;
24
+ /** The language of its title, body and buttons: the person's. */
25
+ readonly lang: Lang;
24
26
  }
25
27
  /** A group member who has not connected: addressed to them, one button only they can use. */
26
28
  export interface OnboardingCardView {
@@ -94,7 +96,8 @@ export declare class CommandViews {
94
96
  readonly note: NoteFactory;
95
97
  constructor(bot: string, publicBaseUrl: string, note: NoteFactory);
96
98
  link(who: string): OutMessage;
97
- binding(confirmUrl: string, lead?: string): OutMessage;
99
+ /** The binding card in the person's language `lang` (its title, body, buttons and notice; `lead` is the caller's). */
100
+ binding(confirmUrl: string, lead?: string, lang?: Lang): OutMessage;
98
101
  onboarding(member: {
99
102
  name: string;
100
103
  channelUserId: string;
@@ -70,9 +70,11 @@ export interface TurnHooks {
70
70
  /**
71
71
  * An approval or question is about to be shown, as `envelope`: false when a card for it is already in a chat (the
72
72
  * notification got there first), and the turn then shows none. `expiresIn`: the seconds Brain waits for the answer.
73
+ * `questions`: a question's questions, as its event carried them (undefined for an approval).
73
74
  */
74
- readonly onApproval?: (sessionId: string, toolUseId: string, tool: string, expiresIn: number | undefined, envelope: Envelope) => boolean | undefined;
75
- readonly onApprovalSent?: (sessionId: string, toolUseId: string) => void;
75
+ readonly onApproval?: (sessionId: string, toolUseId: string, tool: string, expiresIn: number | undefined, envelope: Envelope, questions?: unknown) => boolean | undefined;
76
+ /** The card is in the chat; `cardId`: the chat's id of it, when the chat said it. */
77
+ readonly onApprovalSent?: (sessionId: string, toolUseId: string, cardId?: string) => void;
76
78
  readonly onApprovalUnsent?: (sessionId: string, toolUseId: string) => void;
77
79
  /** (session, "started" | "delivered" | "failed", Claw message id or ''). */
78
80
  readonly onTurn?: (sessionId: string, what: "started" | "delivered" | "failed", messageId: string) => void;
@@ -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>;
@@ -54,6 +54,8 @@ export declare class StatusMessage<S> {
54
54
  private opening;
55
55
  private expiry;
56
56
  private again;
57
+ /** The stream being closed because a card waits for the person (`awaitingChoice`). */
58
+ private choice;
57
59
  private preparingTimer;
58
60
  private lastText;
59
61
  private lastAt;
@@ -80,6 +82,24 @@ export declare class StatusMessage<S> {
80
82
  private statusLines;
81
83
  /** Close the stream with STILL_WORKING before the chat's limit; the answer, whenever it comes, is a message of its own. */
82
84
  private closeInTime;
85
+ /**
86
+ * Close the open stream with `line` (after the agent's words in the bubble) while the turn goes on; the answer, whenever it comes, is
87
+ * a message of its own. Nothing more is sent to the stream once this has begun.
88
+ */
89
+ private closeEarly;
90
+ /**
91
+ * A card now waits for the person (a question of the agent): it is their turn, so the floating status must not go on
92
+ * saying "Thinking…" until the chat's limit. An open stream is closed at once with AWAITING_CHOICE, and the answer comes
93
+ * as a message of its own, as after the limit. Without a stream (the progress message) nothing changes: it has no
94
+ * spinner. Never fails the turn.
95
+ */
96
+ awaitingChoice(): void;
97
+ /**
98
+ * Claw queued the message behind a turn it is still running: `notice` closes the stream at once, so it does not say
99
+ * "Thinking…" for the whole wait. The answer comes as a message of its own, as after the limit. When the stream did not open
100
+ * after all, `notice` is said as a message. False when there is no stream: the caller says it. Never fails the turn.
101
+ */
102
+ queued(notice: string): boolean;
83
103
  /** The chat's id of the posted message; undefined when the post failed or its id never came (nothing is edited then). */
84
104
  private messageId;
85
105
  /**
@@ -24,8 +24,8 @@ export interface TurnApprovals {
24
24
  * False when a card for this approval is already in a chat: the turn then shows none. `userId`: the person the turn is for,
25
25
  * whose approval it is (Python `store.save_approval(sender.aad_id, ...)`).
26
26
  */
27
- record(userId: string, sessionId: string, toolUseId: string, tool: string, expiresIn: number | undefined, envelope: Envelope): boolean;
28
- sent(sessionId: string, toolUseId: string): void;
27
+ record(userId: string, sessionId: string, toolUseId: string, tool: string, expiresIn: number | undefined, envelope: Envelope, questions?: unknown): boolean;
28
+ sent(sessionId: string, toolUseId: string, cardId?: string): void;
29
29
  unsent(sessionId: string, toolUseId: string): void;
30
30
  }
31
31
  /** What a review turn adds to a chat turn (the review service fills it in). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primusclaw/personal-agent",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "The personal agent's channel-neutral core: domain logic, ports and plugin interfaces.",
5
5
  "license": "MIT",
6
6
  "repository": {