@fastagent-sh/fastagent 0.15.0 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -4
- package/dist/bind.d.ts +34 -0
- package/dist/bind.js +74 -0
- package/dist/channels/agentcore-limits.d.ts +7 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-state.d.ts +83 -0
- package/dist/channels/agentcore-state.js +266 -0
- package/dist/channels/agentcore.d.ts +98 -0
- package/dist/channels/agentcore.js +260 -0
- package/dist/channels/busy.d.ts +23 -0
- package/dist/channels/busy.js +53 -0
- package/dist/channels/context-buffer.d.ts +5 -0
- package/dist/channels/context-buffer.js +5 -0
- package/dist/channels/feishu/context-buffer.d.ts +13 -8
- package/dist/channels/feishu/context-buffer.js +60 -6
- package/dist/channels/feishu/feishu-api.js +4 -1
- package/dist/channels/feishu/feishu.d.ts +0 -12
- package/dist/channels/feishu/feishu.js +143 -54
- package/dist/channels/feishu/invoke-turn.js +35 -18
- package/dist/channels/feishu/model.d.ts +0 -1
- package/dist/channels/feishu/normalize.js +0 -1
- package/dist/channels/feishu/parse.d.ts +21 -7
- package/dist/channels/feishu/parse.js +24 -7
- package/dist/channels/feishu/preview.js +3 -2
- package/dist/channels/feishu/scaffold/channel.ts +9 -8
- package/dist/channels/feishu/scaffold/feishu-send.ts +6 -4
- package/dist/channels/feishu/setup-mode.d.ts +30 -1
- package/dist/channels/feishu/setup-mode.js +27 -1
- package/dist/channels/github/github.js +8 -1
- package/dist/channels/http.js +1 -1
- package/dist/channels/lark/scaffold/channel.ts +9 -8
- package/dist/channels/lark/scaffold/lark-send.ts +6 -4
- package/dist/channels/preview-kit.d.ts +7 -1
- package/dist/channels/preview-kit.js +3 -2
- package/dist/channels/slack/parse.d.ts +16 -1
- package/dist/channels/slack/parse.js +46 -3
- package/dist/channels/slack/preview.d.ts +1 -2
- package/dist/channels/slack/preview.js +68 -24
- package/dist/channels/slack/scaffold/channel.ts +5 -5
- package/dist/channels/slack/slack-api.d.ts +3 -23
- package/dist/channels/slack/slack-api.js +6 -22
- package/dist/channels/slack/slack.d.ts +13 -20
- package/dist/channels/slack/slack.js +95 -50
- package/dist/channels/state.d.ts +11 -4
- package/dist/channels/state.js +19 -12
- package/dist/channels/tasks.d.ts +0 -6
- package/dist/channels/tasks.js +16 -1
- package/dist/channels/telegram/parse.d.ts +0 -7
- package/dist/channels/telegram/parse.js +4 -2
- package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
- package/dist/channels/telegram/telegram.js +1 -1
- package/dist/channels/text.d.ts +14 -0
- package/dist/channels/text.js +14 -0
- package/dist/channels/thread-participants.d.ts +21 -0
- package/dist/channels/thread-participants.js +132 -0
- package/dist/channels/turn-queue.js +7 -0
- package/dist/cli/add-feishu.d.ts +7 -4
- package/dist/cli/add-feishu.js +57 -37
- package/dist/cli/add-slack.d.ts +2 -1
- package/dist/cli/add-slack.js +6 -11
- package/dist/cli/commands/add.js +50 -51
- package/dist/cli/commands/attach.js +8 -4
- package/dist/cli/commands/chat.js +8 -8
- package/dist/cli/commands/deploy.d.ts +14 -1
- package/dist/cli/commands/deploy.js +330 -78
- package/dist/cli/commands/dev.d.ts +1 -0
- package/dist/cli/commands/dev.js +38 -25
- package/dist/cli/commands/fire.js +15 -16
- package/dist/cli/commands/info.js +36 -29
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.js +65 -53
- package/dist/cli/commands/invoke.js +9 -6
- package/dist/cli/commands/login.js +35 -21
- package/dist/cli/commands/schedule.js +6 -8
- package/dist/cli/commands/start.d.ts +1 -0
- package/dist/cli/commands/start.js +101 -37
- package/dist/cli/commands/tool.js +30 -18
- package/dist/cli/fail.d.ts +17 -0
- package/dist/cli/fail.js +24 -0
- package/dist/cli/program.js +66 -36
- package/dist/cli/serve.d.ts +52 -8
- package/dist/cli/serve.js +133 -32
- package/dist/cli/shared.d.ts +21 -2
- package/dist/cli/shared.js +44 -18
- package/dist/deploy/agentcore/plan.d.ts +117 -0
- package/dist/deploy/agentcore/plan.js +721 -0
- package/dist/deploy/agentcore/run.d.ts +73 -0
- package/dist/deploy/agentcore/run.js +412 -0
- package/dist/deploy/agentcore/zip.d.ts +17 -0
- package/dist/deploy/agentcore/zip.js +68 -0
- package/dist/deploy/container.d.ts +26 -25
- package/dist/deploy/container.js +93 -89
- package/dist/deploy/docker/plan.d.ts +1 -1
- package/dist/deploy/docker/plan.js +12 -17
- package/dist/deploy/fly/plan.d.ts +2 -0
- package/dist/deploy/fly/plan.js +27 -19
- package/dist/deploy/fly/run.d.ts +12 -1
- package/dist/deploy/fly/run.js +36 -2
- package/dist/deploy/preflight.d.ts +11 -5
- package/dist/deploy/preflight.js +235 -65
- package/dist/deploy/railway/plan.d.ts +7 -0
- package/dist/deploy/railway/plan.js +41 -16
- package/dist/deploy/railway/run.d.ts +8 -1
- package/dist/deploy/railway/run.js +7 -2
- package/dist/deploy/runner.d.ts +5 -2
- package/dist/deploy/runner.js +9 -3
- package/dist/dev-supervisor.d.ts +11 -8
- package/dist/dev-supervisor.js +53 -51
- package/dist/engines/pi/auth.d.ts +8 -7
- package/dist/engines/pi/auth.js +12 -10
- package/dist/engines/pi/channel.d.ts +1 -1
- package/dist/engines/pi/channel.js +5 -5
- package/dist/engines/pi/chat.js +2 -2
- package/dist/engines/pi/config.d.ts +12 -48
- package/dist/engines/pi/config.js +29 -110
- package/dist/engines/pi/create.d.ts +32 -24
- package/dist/engines/pi/create.js +47 -23
- package/dist/engines/pi/definition.d.ts +7 -26
- package/dist/engines/pi/definition.js +8 -54
- package/dist/engines/pi/harness.d.ts +19 -5
- package/dist/engines/pi/harness.js +3 -5
- package/dist/engines/pi/login.d.ts +1 -1
- package/dist/engines/pi/models.d.ts +3 -3
- package/dist/engines/pi/models.js +1 -1
- package/dist/engines/pi/{workspace.d.ts → open.d.ts} +31 -25
- package/dist/engines/pi/{workspace.js → open.js} +27 -29
- package/dist/engines/pi/read-image.d.ts +4 -0
- package/dist/engines/pi/read-image.js +62 -0
- package/dist/engines/pi/search-tools.d.ts +6 -4
- package/dist/engines/pi/search-tools.js +3 -1
- package/dist/engines/pi/session-builder.d.ts +2 -2
- package/dist/engines/pi/session-builder.js +18 -13
- package/dist/engines/pi/tool.d.ts +13 -5
- package/dist/engines/pi/tool.js +4 -0
- package/dist/engines/pi/wake-tool.d.ts +3 -3
- package/dist/env.d.ts +16 -4
- package/dist/env.js +43 -5
- package/dist/host/node.d.ts +4 -2
- package/dist/host/node.js +2 -1
- package/dist/loader.d.ts +2 -2
- package/dist/loader.js +3 -3
- package/dist/log.d.ts +1 -1
- package/dist/log.js +1 -1
- package/dist/paths.d.ts +138 -0
- package/dist/paths.js +326 -0
- package/dist/pi.d.ts +2 -2
- package/dist/pi.js +2 -2
- package/dist/runtime.d.ts +7 -5
- package/dist/runtime.js +2 -2
- package/dist/scaffold/add-channel.d.ts +7 -3
- package/dist/scaffold/add-channel.js +55 -29
- package/dist/scaffold/init.d.ts +32 -41
- package/dist/scaffold/init.js +161 -185
- package/dist/scaffold/templates/env.example +15 -6
- package/dist/scaffold/templates/fastagent.config.mjs +1 -1
- package/dist/scaffold/templates/gitignore +14 -6
- package/dist/scaffold/templates/persona.md +4 -2
- package/dist/scaffold/templates/secrets.gitignore +5 -0
- package/dist/scaffold/templates.d.ts +1 -7
- package/dist/scaffold/templates.js +3 -25
- package/dist/scaffold/vendor-skill.d.ts +2 -2
- package/dist/scaffold/vendor-skill.js +13 -13
- package/dist/schedule/discover.js +4 -4
- package/dist/schedule/scheduler.d.ts +40 -1
- package/dist/schedule/scheduler.js +89 -56
- package/dist/schedule/state.js +1 -1
- package/dist/schedule/wake-alarm.d.ts +47 -0
- package/dist/schedule/wake-alarm.js +136 -0
- package/dist/schedule/wakeups.d.ts +1 -0
- package/dist/schedule/wakeups.js +18 -0
- package/dist/tunnel.d.ts +3 -3
- package/dist/tunnel.js +7 -7
- package/package.json +7 -4
- package/dist/channels/feishu/owned-threads.d.ts +0 -7
- package/dist/channels/feishu/owned-threads.js +0 -47
- package/dist/channels/slack/owned-threads.d.ts +0 -6
- package/dist/channels/slack/owned-threads.js +0 -43
- package/dist/scaffold/templates/gitignore.agentdir-root +0 -5
- package/dist/scaffold/templates/gitignore.kit +0 -2
- package/dist/workspace.d.ts +0 -9
- package/dist/workspace.js +0 -45
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { ChannelModule } from "../../host/node.ts";
|
|
2
2
|
import { type SlackEventEnvelope, type SlackFile, type SlackMessageEvent, type SlackRoute, defaultSlackRoute, slackEnvelope } from "./parse.ts";
|
|
3
3
|
import { type SlackFailure, type SlackRendering } from "./preview.ts";
|
|
4
|
-
import { type SlackTaskDisplayMode } from "./slack-api.ts";
|
|
5
4
|
export { defaultSlackRoute, slackEnvelope };
|
|
6
5
|
export type { SlackEventEnvelope, SlackFailure, SlackFile, SlackMessageEvent, SlackRendering, SlackRoute };
|
|
7
6
|
export interface SlackChannelOptions {
|
|
@@ -14,25 +13,19 @@ export interface SlackChannelOptions {
|
|
|
14
13
|
clientId?: string;
|
|
15
14
|
clientSecret?: string;
|
|
16
15
|
botTokenExpiresAt?: number;
|
|
17
|
-
/**
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* own session/thread; `continuous` keeps one session for channel-top-level turns while preserving
|
|
22
|
-
* existing Slack threads as separate root sessions. */
|
|
23
|
-
groupMessageSession?: "continuous" | "threaded";
|
|
24
|
-
/** `context` (default) admits bare replies in managed group threads and buffers unsummoned group
|
|
25
|
-
* discussion. `mentions` answers only app_mention plus DMs for an explicit least-privilege setup. */
|
|
16
|
+
/** `context` (default) subscribes to group message streams, which is what lets the channel HEAR a
|
|
17
|
+
* thread: bare replies are then admitted by the participation rule (design/participant-model.md §3),
|
|
18
|
+
* and other discussion is buffered. `mentions` answers only app_mention plus DMs for an explicit
|
|
19
|
+
* least-privilege setup. */
|
|
26
20
|
groupBehavior?: "context" | "mentions";
|
|
27
|
-
/** `native` (default) uses Slack Agent streams
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
21
|
+
/** `native` (default) uses Slack Agent streams for threaded replies. Its inline tool traces carry a
|
|
22
|
+
* bounded summary of each call's first argument and cannot be retracted, so they stay in the
|
|
23
|
+
* delivered message beside the answer. `classic` retains the compatibility renderer based on one
|
|
24
|
+
* rate-limited edited message, which settles into the answer alone. A top-level target necessarily
|
|
25
|
+
* uses the classic renderer, because Slack streams require a parent user message; a custom route
|
|
26
|
+
* reaches one either by returning `threadTs: null` or by redirecting to another channel without
|
|
27
|
+
* naming a thread. */
|
|
31
28
|
rendering?: SlackRendering;
|
|
32
|
-
/** Native task-card layout (`chat.startStream` `task_display_mode`): `plan` (default) groups steps
|
|
33
|
-
* under a single collapsible heading, `timeline` lists each step sequentially, `dense` collapses
|
|
34
|
-
* consecutive tool calls into one summarized card. Applies to the native renderer only. */
|
|
35
|
-
taskDisplay?: SlackTaskDisplayMode;
|
|
36
29
|
/** Optional footer for successful Agent replies. Omitted or `false` sends no repetitive disclaimer. */
|
|
37
30
|
aiDisclaimer?: string | false;
|
|
38
31
|
/** First-run direct-message welcome, sent once when a user first opens the DM (`app_home_opened`,
|
|
@@ -46,7 +39,7 @@ export interface SlackChannelOptions {
|
|
|
46
39
|
processing?: string;
|
|
47
40
|
completed?: string;
|
|
48
41
|
};
|
|
49
|
-
/** Custom route policy. Providing it disables the default
|
|
42
|
+
/** Custom route policy. Providing it disables the default participant-model thread/context admission policy. */
|
|
50
43
|
route?: (envelope: SlackEventEnvelope) => SlackRoute | null;
|
|
51
44
|
/** Customer-facing failure formatter; full details always remain in operator logs. */
|
|
52
45
|
onError?: (failure: SlackFailure) => string | undefined;
|
|
@@ -55,4 +48,4 @@ export interface SlackChannelOptions {
|
|
|
55
48
|
}
|
|
56
49
|
/** Verify Slack's v0 HMAC over the exact raw body and reject timestamps outside the replay window. */
|
|
57
50
|
export declare function verifySlackSignature(signingSecret: string, timestamp: string, signature: string, rawBody: string, nowMs?: number): boolean;
|
|
58
|
-
export declare function slackChannel(
|
|
51
|
+
export declare function slackChannel(options: SlackChannelOptions): ChannelModule;
|
|
@@ -5,8 +5,9 @@ import { log } from "../../log.js";
|
|
|
5
5
|
import { readBodyCapped } from "../body.js";
|
|
6
6
|
import { text } from "../respond.js";
|
|
7
7
|
import { createSeenRing } from "../seen.js";
|
|
8
|
+
import { createThreadParticipants } from "../thread-participants.js";
|
|
8
9
|
import { createTaskTracker } from "../tasks.js";
|
|
9
|
-
import { ensureStateHome } from "../state.js";
|
|
10
|
+
import { ensureStateHome, removeRetiredStateFile } from "../state.js";
|
|
10
11
|
import { dispatchStop, isStopText } from "../stop-command.js";
|
|
11
12
|
import { codePointPrefix } from "../text.js";
|
|
12
13
|
import { createTurnQueue } from "../turn-queue.js";
|
|
@@ -14,8 +15,7 @@ import { createTurnStore } from "../turn-store.js";
|
|
|
14
15
|
import { createSlackBotTokenProvider } from "./bot-auth.js";
|
|
15
16
|
import { collectSlackBufferedFiles, createSlackContextBuffer } from "./context-buffer.js";
|
|
16
17
|
import { invokeSlackTurn } from "./invoke-turn.js";
|
|
17
|
-
import {
|
|
18
|
-
import { defaultSlackRoute, isSlackDirectMessage, isSlackGroupMessage, isSlackHumanMessage, slackBufferText, slackEnvelope, slackFileIds, slackMessageText, slackPlaceKey, slackSenderLabel, slackTeamId, } from "./parse.js";
|
|
18
|
+
import { defaultSlackRoute, isSlackDirectMessage, isSlackGroupMessage, hasSlackMention, hasSlackUserMention, isSlackHumanMessage, mentionsSlackUser, stripSlackMentions, slackBufferText, slackEnvelope, slackFileIds, slackMessageText, slackPlaceKey, slackSenderLabel, slackTeamId, } from "./parse.js";
|
|
19
19
|
import { defaultErrorMessage, settleSlackPreview, streamSlackReply, } from "./preview.js";
|
|
20
20
|
import { resolveReactionEmojis, startSlackReaction } from "./reaction.js";
|
|
21
21
|
import { createSlackApi } from "./slack-api.js";
|
|
@@ -61,12 +61,16 @@ function messageRefOf(turnId) {
|
|
|
61
61
|
const parts = turnId.split(":");
|
|
62
62
|
return parts.length === 3 && parts[1] && parts[2] ? { channelId: parts[1], ts: parts[2] } : undefined;
|
|
63
63
|
}
|
|
64
|
-
export function slackChannel(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
export function slackChannel(options) {
|
|
65
|
+
const { botToken, signingSecret, botRefreshToken, clientId, clientSecret, botTokenExpiresAt, groupBehavior = "context", rendering = "native", aiDisclaimer, welcome = DEFAULT_WELCOME, reactionAck = {}, route, onError, apiBaseUrl = "https://slack.com/api", } = options;
|
|
66
|
+
// The participant model derives placement instead of selecting it: Slack has no quote primitive, so
|
|
67
|
+
// answering in place means answering in a thread on the ask, whichever renderer draws it. An
|
|
68
|
+
// upgraded workspace still passing one of the removed modes would otherwise start fine and silently
|
|
69
|
+
// get a different placement AND a different memory boundary.
|
|
70
|
+
const removedModes = ["directMessageSession", "groupMessageSession"].filter((name) => options[name] !== undefined);
|
|
71
|
+
if (removedModes.length > 0) {
|
|
72
|
+
throw new Error(`slackChannel no longer accepts ${removedModes.join(" / ")}: an answer goes in a thread on the ask, and ` +
|
|
73
|
+
"that thread is the session — see docs/design/participant-model.md");
|
|
70
74
|
}
|
|
71
75
|
if (!["context", "mentions"].includes(groupBehavior)) {
|
|
72
76
|
throw new Error('slackChannel groupBehavior must be "context" or "mentions"');
|
|
@@ -74,9 +78,6 @@ export function slackChannel({ botToken, signingSecret, botRefreshToken, clientI
|
|
|
74
78
|
if (!["native", "classic"].includes(rendering)) {
|
|
75
79
|
throw new Error('slackChannel rendering must be "native" or "classic"');
|
|
76
80
|
}
|
|
77
|
-
if (!["timeline", "plan", "dense"].includes(taskDisplay)) {
|
|
78
|
-
throw new Error('slackChannel taskDisplay must be "timeline", "plan", or "dense"');
|
|
79
|
-
}
|
|
80
81
|
if (welcome !== false && typeof welcome !== "string") {
|
|
81
82
|
throw new Error("slackChannel welcome must be a string or false");
|
|
82
83
|
}
|
|
@@ -134,7 +135,16 @@ export function slackChannel({ botToken, signingSecret, botRefreshToken, clientI
|
|
|
134
135
|
});
|
|
135
136
|
};
|
|
136
137
|
const seen = createSeenRing(join(stateHome, "seen.json"), label);
|
|
137
|
-
const
|
|
138
|
+
const threadParticipants = createThreadParticipants(join(stateHome, "thread-participants.json"), label);
|
|
139
|
+
/** A thread's participation is keyed by the SESSION it describes — "the agent answered here" is a
|
|
140
|
+
* claim about a memory, so the two must not be re-keyable independently. What keeps the claim true
|
|
141
|
+
* under a custom route is the `routed.session === undefined` condition on the write, not the
|
|
142
|
+
* absence of a route: a route that supplies its own session records nothing here. */
|
|
143
|
+
const threadKey = (teamId, channelId, threadTs) => `slack:${teamId}:${channelId}:${threadTs}`;
|
|
144
|
+
// The participant model replaced the owned-thread index (a cache, so nothing is lost). REMOVE THIS
|
|
145
|
+
// after the release following the participant model ships — by then no live deployment can still
|
|
146
|
+
// be carrying the file. test/migration-deadline.test.ts fails when due.
|
|
147
|
+
removeRetiredStateFile(stateHome, "owned-threads.json", label);
|
|
138
148
|
const welcomed = createWelcomedUsers(join(stateHome, "welcomed.json"), label);
|
|
139
149
|
const buffer = createSlackContextBuffer(join(stateHome, "buffers.json"), label);
|
|
140
150
|
const store = createTurnStore(join(stateHome, "turns.json"), {
|
|
@@ -220,7 +230,6 @@ export function slackChannel({ botToken, signingSecret, botRefreshToken, clientI
|
|
|
220
230
|
initialPreviewTs: turn.previewTs,
|
|
221
231
|
threadTitle: turn.threadTitle,
|
|
222
232
|
disclaimer: aiDisclaimer,
|
|
223
|
-
taskDisplay,
|
|
224
233
|
label,
|
|
225
234
|
});
|
|
226
235
|
log.info(`${label} turn done: turn=${turn.id} session=${turn.session} (${Date.now() - startedAt}ms)`);
|
|
@@ -254,6 +263,9 @@ export function slackChannel({ botToken, signingSecret, botRefreshToken, clientI
|
|
|
254
263
|
let seq = recovered.reduce((maximum, turn) => Math.max(maximum, turn.seq), 0);
|
|
255
264
|
for (const { attempts: _attempts, ...intent } of recovered)
|
|
256
265
|
submit({ ...intent }, false);
|
|
266
|
+
// Acceptance touches no network, so it stays synchronous inside Slack's ACK window and the
|
|
267
|
+
// delivery dedup ring alone is enough — there is no await for a duplicate delivery to race
|
|
268
|
+
// through. The minutes-long Agent turn remains fire-and-forget.
|
|
257
269
|
const acceptEvent = (envelope) => {
|
|
258
270
|
const event = envelope.event;
|
|
259
271
|
if (!isSlackHumanMessage(event))
|
|
@@ -274,24 +286,44 @@ export function slackChannel({ botToken, signingSecret, botRefreshToken, clientI
|
|
|
274
286
|
const direct = isSlackDirectMessage(event);
|
|
275
287
|
const rootTs = event.thread_ts ?? event.ts;
|
|
276
288
|
const bufferKey = slackPlaceKey(teamId, event);
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
289
|
+
// Listening is not speaking: every message the channel can see refines who takes part in its
|
|
290
|
+
// thread, whether or not it is answered. Humans only — a bot's own posts are recorded where they
|
|
291
|
+
// are known — this channel answering.
|
|
292
|
+
// Structural facts only, never `groupBehavior` or `route` — see thread-participants.ts. Slack
|
|
293
|
+
// adds no delta of its own here; its summon rule is the only consumer.
|
|
294
|
+
if (group && event.thread_ts !== undefined) {
|
|
295
|
+
threadParticipants.merge(threadKey(teamId, event.channel, event.thread_ts), { humans: [event.user] });
|
|
296
|
+
}
|
|
283
297
|
let routed = decide(envelope);
|
|
284
|
-
|
|
285
|
-
|
|
298
|
+
// Two different questions (see parse.ts). `addressesSomeone` is the "@-mentions only other people
|
|
299
|
+
// is discussion, never an ask" guard (§3) and counts broadcasts and user groups too;
|
|
300
|
+
// `mightBeTheBot` gates the identity-window deferral below, where only a USER mention qualifies.
|
|
301
|
+
const addressesSomeone = hasSlackMention(event.text ?? "");
|
|
302
|
+
const mightBeTheBot = hasSlackUserMention(event.text ?? "");
|
|
303
|
+
const structurallyMentionsBot = botUserId !== undefined && mentionsSlackUser(event.text ?? "", botUserId);
|
|
286
304
|
// app_mention and message.* subscriptions can overlap. If message.* arrives first, structural bot
|
|
287
305
|
// identity routes it now; while auth.test is still unresolved, defer any mentioned message rather
|
|
288
306
|
// than buffer+dedup it and accidentally suppress the later app_mention callback.
|
|
289
307
|
if (!routed && route === undefined && group && event.type === "message" && structurallyMentionsBot)
|
|
290
308
|
routed = {};
|
|
291
|
-
|
|
309
|
+
// The participant model's thread rule (§3): a bare message reaches the agent while it takes part
|
|
310
|
+
// and has not heard a second human. Note the ORDER — the mention guard below runs first and is
|
|
311
|
+
// structural: it reads who THIS message addresses, which is the one thing an observation-only
|
|
312
|
+
// store cannot know about someone it has never heard.
|
|
313
|
+
if (!routed &&
|
|
314
|
+
groupBehavior === "context" &&
|
|
315
|
+
route === undefined &&
|
|
316
|
+
group &&
|
|
317
|
+
event.thread_ts !== undefined &&
|
|
318
|
+
event.type !== "app_mention" &&
|
|
319
|
+
// Mentioning only other people is targeted discussion, never an ask (§3) — the same guard
|
|
320
|
+
// Feishu applies with `hasMentions`.
|
|
321
|
+
!addressesSomeone &&
|
|
322
|
+
threadParticipants.admitsBareMessage(threadKey(teamId, event.channel, event.thread_ts))) {
|
|
292
323
|
routed = {};
|
|
324
|
+
}
|
|
293
325
|
if (!routed) {
|
|
294
|
-
if (route === undefined && group && botUserId === undefined &&
|
|
326
|
+
if (route === undefined && group && botUserId === undefined && mightBeTheBot)
|
|
295
327
|
return;
|
|
296
328
|
if (groupBehavior === "context" && route === undefined && group) {
|
|
297
329
|
const body = slackBufferText(slackMessageText(event));
|
|
@@ -312,22 +344,17 @@ export function slackChannel({ botToken, signingSecret, botRefreshToken, clientI
|
|
|
312
344
|
}
|
|
313
345
|
const targetChannel = routed.channelId ?? event.channel;
|
|
314
346
|
const sameChannel = targetChannel === event.channel;
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
347
|
+
// Answer where asked (participant model §4). Slack has no quote primitive, so the only way to
|
|
348
|
+
// attach an answer to its question is a thread on it — which then IS the place, and carries the
|
|
349
|
+
// memory (§5). Native streaming additionally REQUIRES a thread, but the shape does not depend on
|
|
350
|
+
// the renderer: `classic` attaches its answer the same way.
|
|
351
|
+
const defaultThread = event.thread_ts ?? event.ts;
|
|
318
352
|
const threadTs = routed.threadTs === null ? undefined : (routed.threadTs ?? (sameChannel ? defaultThread : undefined));
|
|
319
|
-
const
|
|
320
|
-
const defaultSession = direct
|
|
321
|
-
? directMessageSession === "continuous" && continuousTopLevel
|
|
322
|
-
? `slack:${teamId}:${event.channel}`
|
|
323
|
-
: `slack:${teamId}:${event.channel}:${rootTs}`
|
|
324
|
-
: groupMessageSession === "continuous" && continuousTopLevel
|
|
325
|
-
? `slack:${teamId}:${event.channel}`
|
|
326
|
-
: `slack:${teamId}:${event.channel}:${rootTs}`;
|
|
353
|
+
const defaultSession = threadKey(teamId, event.channel, rootTs);
|
|
327
354
|
// Explicit user stop: a control action, never a turn — it must not queue behind the run it
|
|
328
355
|
// stops. Match the bare word after stripping the bot mention; record the logical id so a Slack
|
|
329
356
|
// redelivery doesn't double-abort or double-notify.
|
|
330
|
-
if (isStopText((event.text ?? "")
|
|
357
|
+
if (isStopText(stripSlackMentions(event.text ?? ""))) {
|
|
331
358
|
seen.add(logicalId);
|
|
332
359
|
const target = { channelId: event.channel, threadTs: event.thread_ts };
|
|
333
360
|
sideTasks.track(dispatchStop(control, routed.session ?? defaultSession, label)
|
|
@@ -340,21 +367,8 @@ export function slackChannel({ botToken, signingSecret, botRefreshToken, clientI
|
|
|
340
367
|
if (!baseText.trim() && fileIds.length === 0)
|
|
341
368
|
return;
|
|
342
369
|
const threadTitle = direct && event.thread_ts === undefined
|
|
343
|
-
? codePointPrefix(slackMessageText(event)
|
|
344
|
-
.replace(/<@[A-Z0-9]+>/gi, "")
|
|
345
|
-
.replace(/\s+/g, " ")
|
|
346
|
-
.trim(), 80)
|
|
370
|
+
? codePointPrefix(stripSlackMentions(slackMessageText(event), "").replace(/\s+/g, " ").trim(), 80)
|
|
347
371
|
: undefined;
|
|
348
|
-
// Match Feishu/Lark ownership in context mode: only a top-level summon that creates an
|
|
349
|
-
// Agent-managed thread owns the root. Mentioning the Agent inside an existing human thread
|
|
350
|
-
// answers once without adopting it; mention-only mode never consumes ownership state.
|
|
351
|
-
if (groupBehavior === "context" &&
|
|
352
|
-
route === undefined &&
|
|
353
|
-
threadedGroup &&
|
|
354
|
-
event.thread_ts === undefined &&
|
|
355
|
-
sameChannel) {
|
|
356
|
-
ownedThreads.add(teamId, event.channel, rootTs);
|
|
357
|
-
}
|
|
358
372
|
submit({
|
|
359
373
|
id: logicalId,
|
|
360
374
|
seq: ++seq,
|
|
@@ -368,6 +382,37 @@ export function slackChannel({ botToken, signingSecret, botRefreshToken, clientI
|
|
|
368
382
|
threadTitle: threadTitle || undefined,
|
|
369
383
|
fileIds,
|
|
370
384
|
}, true);
|
|
385
|
+
// Answering inside a GROUP thread makes the agent a participant of it, which is what lets the
|
|
386
|
+
// NEXT bare message address it without a mention.
|
|
387
|
+
//
|
|
388
|
+
// `group` excludes DMs, whose `threadTs` is always defined (the answer opens its assistant
|
|
389
|
+
// thread) and which no rule could ever read — safe because it is structural: a channel never
|
|
390
|
+
// becomes a DM.
|
|
391
|
+
//
|
|
392
|
+
// The two `routed` conditions keep the record describing what it claims. `session` undefined: the
|
|
393
|
+
// flag asserts "the agent answered into THIS thread's session", so a route supplying its own
|
|
394
|
+
// would record participation in a memory that never held the turn. `threadTs` undefined: a route
|
|
395
|
+
// can send the answer to a DIFFERENT thread, where the asker never spoke — recording them there
|
|
396
|
+
// would invent a participant. Feishu carries the same two conditions for the same reasons.
|
|
397
|
+
//
|
|
398
|
+
// Recorded only once the intent is durable: `submit` can throw, and a redelivery must still see
|
|
399
|
+
// the thread as the agent has actually left it.
|
|
400
|
+
if (group &&
|
|
401
|
+
threadTs !== undefined &&
|
|
402
|
+
sameChannel &&
|
|
403
|
+
routed.session === undefined &&
|
|
404
|
+
routed.threadTs === undefined) {
|
|
405
|
+
// The ASKER counts as heard in this thread too, and both halves are written together so the
|
|
406
|
+
// record can never say "the agent takes part and nobody has spoken". When the ask is top
|
|
407
|
+
// level, the answer is what CREATES the thread, so the observation above never ran for it (no
|
|
408
|
+
// `thread_ts` on the ask) — without this, a thread whose root is a human would not count them,
|
|
409
|
+
// and a stranger's first bare reply would read as a two-party exchange. `event.user` is
|
|
410
|
+
// guaranteed here (isSlackHumanMessage). Idempotent when the ask was already in the thread.
|
|
411
|
+
threadParticipants.merge(threadKey(teamId, event.channel, threadTs), {
|
|
412
|
+
agentSpoke: true,
|
|
413
|
+
humans: [event.user],
|
|
414
|
+
});
|
|
415
|
+
}
|
|
371
416
|
};
|
|
372
417
|
// Side tasks (stop feedback, DM welcomes) run off the ACK path but drain in turnsIdle.
|
|
373
418
|
const sideTasks = createTaskTracker();
|
package/dist/channels/state.d.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
/** Create the state home
|
|
2
|
-
* files) can carry chat content and must never be committable. The workspace opener already protects
|
|
3
|
-
* an in-tree state root; this local guard also covers direct embedders. `wx` never clobbers an
|
|
4
|
-
* operator's own file. */
|
|
1
|
+
/** Create the channel's state home — the one shared spelling of it, so no channel invents its own. */
|
|
5
2
|
export declare function ensureStateHome(dir: string): void;
|
|
6
3
|
/** Returns `unknown` on purpose — no generic pretending otherwise: the file is an IO boundary, and the
|
|
7
4
|
* caller owns shape validation (a `<T>` here would be an unchecked cast wearing a type). */
|
|
8
5
|
export declare function loadStateFile(path: string): unknown;
|
|
9
6
|
export declare function saveStateFile(path: string, value: unknown): void;
|
|
7
|
+
/**
|
|
8
|
+
* Drop a state file a redesign retired. Best-effort by design: a leftover file is untidy, not fatal,
|
|
9
|
+
* so a failure is debug-level and never blocks a boot. Only for files that are pure CACHE — anything
|
|
10
|
+
* whose loss changes behaviour needs a migration, not a delete.
|
|
11
|
+
*
|
|
12
|
+
* Shared because a retired file is usually retired in every channel at once: one best-effort
|
|
13
|
+
* semantic, one log shape, one place to check what "retired" means here. (The removal DEADLINE is not
|
|
14
|
+
* here — it lives in test/migration-deadline.test.ts, which names every call site to delete.)
|
|
15
|
+
*/
|
|
16
|
+
export declare function removeRetiredStateFile(stateHome: string, name: string, label: string): void;
|
package/dist/channels/state.js
CHANGED
|
@@ -12,22 +12,12 @@
|
|
|
12
12
|
* is an ENVIRONMENT error the operator must fix: it throws, and construction fails loudly — booting
|
|
13
13
|
* with silently-empty state would hide real data behind a config mistake.
|
|
14
14
|
*/
|
|
15
|
-
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { dirname, join } from "node:path";
|
|
17
17
|
import { log } from "../log.js";
|
|
18
|
-
/** Create the state home
|
|
19
|
-
* files) can carry chat content and must never be committable. The workspace opener already protects
|
|
20
|
-
* an in-tree state root; this local guard also covers direct embedders. `wx` never clobbers an
|
|
21
|
-
* operator's own file. */
|
|
18
|
+
/** Create the channel's state home — the one shared spelling of it, so no channel invents its own. */
|
|
22
19
|
export function ensureStateHome(dir) {
|
|
23
20
|
mkdirSync(dir, { recursive: true });
|
|
24
|
-
try {
|
|
25
|
-
writeFileSync(join(dir, ".gitignore"), "*\n", { flag: "wx" });
|
|
26
|
-
}
|
|
27
|
-
catch (e) {
|
|
28
|
-
if (e.code !== "EEXIST")
|
|
29
|
-
throw e;
|
|
30
|
-
}
|
|
31
21
|
}
|
|
32
22
|
/** Returns `unknown` on purpose — no generic pretending otherwise: the file is an IO boundary, and the
|
|
33
23
|
* caller owns shape validation (a `<T>` here would be an unchecked cast wearing a type). */
|
|
@@ -58,3 +48,20 @@ export function saveStateFile(path, value) {
|
|
|
58
48
|
writeFileSync(tmp, JSON.stringify(value));
|
|
59
49
|
renameSync(tmp, path);
|
|
60
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Drop a state file a redesign retired. Best-effort by design: a leftover file is untidy, not fatal,
|
|
53
|
+
* so a failure is debug-level and never blocks a boot. Only for files that are pure CACHE — anything
|
|
54
|
+
* whose loss changes behaviour needs a migration, not a delete.
|
|
55
|
+
*
|
|
56
|
+
* Shared because a retired file is usually retired in every channel at once: one best-effort
|
|
57
|
+
* semantic, one log shape, one place to check what "retired" means here. (The removal DEADLINE is not
|
|
58
|
+
* here — it lives in test/migration-deadline.test.ts, which names every call site to delete.)
|
|
59
|
+
*/
|
|
60
|
+
export function removeRetiredStateFile(stateHome, name, label) {
|
|
61
|
+
try {
|
|
62
|
+
rmSync(join(stateHome, name), { force: true });
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
log.debug(`${label} could not remove the obsolete ${name}: ${String(error)}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
package/dist/channels/tasks.d.ts
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SHARED fire-and-forget side-task tracking. Channels launch work off the request path (stop
|
|
3
|
-
* feedback, DM welcomes) that must not block the transport ACK but MUST be drained on shutdown
|
|
4
|
-
* (`turnsIdle`) — otherwise a reply in flight when the process exits is silently dropped. Error
|
|
5
|
-
* handling stays with the caller: track() only guarantees the drain sees the task settle.
|
|
6
|
-
*/
|
|
7
1
|
export interface TaskTracker {
|
|
8
2
|
/** Track one task. The caller keeps its own `.catch` — rejections must already be handled. */
|
|
9
3
|
track(task: Promise<unknown>): void;
|
package/dist/channels/tasks.js
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHARED fire-and-forget side-task tracking. Channels launch work off the request path (stop
|
|
3
|
+
* feedback, DM welcomes) that must not block the transport ACK but MUST be drained on shutdown
|
|
4
|
+
* (`turnsIdle`) — otherwise a reply in flight when the process exits is silently dropped. Error
|
|
5
|
+
* handling stays with the caller: track() only guarantees the drain sees the task settle.
|
|
6
|
+
*/
|
|
7
|
+
import { beginWork } from "./busy.js";
|
|
1
8
|
export function createTaskTracker() {
|
|
2
9
|
const tasks = new Set();
|
|
3
10
|
return {
|
|
4
11
|
track(task) {
|
|
5
12
|
tasks.add(task);
|
|
6
|
-
|
|
13
|
+
// Tracked side tasks count as process-wide in-flight work (busy.ts) — same signal the turn
|
|
14
|
+
// queue reports, read by serving surfaces that must not idle while background work runs.
|
|
15
|
+
const workDone = beginWork();
|
|
16
|
+
void task
|
|
17
|
+
.finally(() => {
|
|
18
|
+
workDone();
|
|
19
|
+
tasks.delete(task);
|
|
20
|
+
})
|
|
21
|
+
.catch(() => { }); // the caller's chain owns the error
|
|
7
22
|
},
|
|
8
23
|
drain: () => Promise.all(tasks).then(() => undefined),
|
|
9
24
|
};
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Telegram protocol parsing — PURE: message field extraction, the prompt envelope, and the summon/route
|
|
3
|
-
* policy. The defining invariant is purity: no state, no IO, no Bot API calls — plain data-in → data-out.
|
|
4
|
-
* In telegram.ts's pipeline (verify → decide via `route` → run the turn → stream reply), this is the
|
|
5
|
-
* "decide" and prompt-building half; telegram.ts wires it in and owns the stateful lifecycle. Kept
|
|
6
|
-
* separate so this layer tests as plain functions and reads without the factory's noise.
|
|
7
|
-
*/
|
|
8
1
|
/** A Telegram message (the common subset; `[k]` keeps the rest reachable without a types dependency). */
|
|
9
2
|
export interface TelegramMessage {
|
|
10
3
|
message_id: number;
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* "decide" and prompt-building half; telegram.ts wires it in and owns the stateful lifecycle. Kept
|
|
6
6
|
* separate so this layer tests as plain functions and reads without the factory's noise.
|
|
7
7
|
*/
|
|
8
|
+
import { BUFFER_LINE_MAX_CHARS } from "../context-buffer.js";
|
|
9
|
+
import { REFERENT_MAX_CODE_POINTS, truncateCodePointPrefix } from "../text.js";
|
|
8
10
|
/** The actionable message in an update (a fresh message or channel post). Edits (`edited_message` /
|
|
9
11
|
* `edited_channel_post`) are deliberately NOT actionable: answering them re-answers every typo fix (a
|
|
10
12
|
* duplicate reply per edit), so an edited message changes nothing — the standard bot behavior. The
|
|
@@ -62,7 +64,7 @@ function bodyOf(m) {
|
|
|
62
64
|
}
|
|
63
65
|
/** A one-line, length-capped rendering of a message's content for the context buffer. */
|
|
64
66
|
export function messageText(m) {
|
|
65
|
-
return (bodyOf(m) ?? "").replace(/\s+/g, " ").trim()
|
|
67
|
+
return truncateCodePointPrefix((bodyOf(m) ?? "").replace(/\s+/g, " ").trim(), BUFFER_LINE_MAX_CHARS);
|
|
66
68
|
}
|
|
67
69
|
/**
|
|
68
70
|
* The default base prompt: a context envelope (chat/thread/sender + a group note + reply) then the
|
|
@@ -88,7 +90,7 @@ export function telegramEnvelope(m) {
|
|
|
88
90
|
const isGroup = m.chat.type === "group" || m.chat.type === "supergroup";
|
|
89
91
|
const scope = isGroup ? "\n[group chat — multiple people; each message is prefixed with its sender]" : "";
|
|
90
92
|
const replyTo = r
|
|
91
|
-
? `\n[in reply to ${fromLabel(r.from) ?? `msg ${r.message_id}`} (msg ${r.message_id}): ${(bodyOf(r) ?? "(empty)"
|
|
93
|
+
? `\n[in reply to ${fromLabel(r.from) ?? `msg ${r.message_id}`} (msg ${r.message_id}): ${truncateCodePointPrefix(bodyOf(r) ?? "(empty)", REFERENT_MAX_CODE_POINTS)}]`
|
|
92
94
|
: "";
|
|
93
95
|
const parts = [bodyOf(m) ?? ""];
|
|
94
96
|
if (m.location)
|
|
@@ -26,7 +26,7 @@ function splitPlain(text: string): string[] {
|
|
|
26
26
|
return parts;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
//
|
|
29
|
+
// Embedded copy of the channel transport's discipline: an upload timeout so a wedged connection
|
|
30
30
|
// can't hang the tool call (and the turn), named errors, and success gated on the body's own ok.
|
|
31
31
|
// Deliberately NO 429 retry — a tool error goes back to the agent, which can decide to retry;
|
|
32
32
|
// fail-fast beats a silently sleeping tool.
|
|
@@ -60,8 +60,11 @@ export default defineTool({
|
|
|
60
60
|
description:
|
|
61
61
|
"Send to a Telegram chat: a text message (`text` — long text is split into multiple messages " +
|
|
62
62
|
"automatically), or a local file (`path` — a document, or a photo if it is an image). Exactly one " +
|
|
63
|
-
"of text/path.
|
|
64
|
-
"
|
|
63
|
+
"of text/path. Use it for a turn NO channel is carrying — a scheduled or self-scheduled (wake) " +
|
|
64
|
+
"turn — or to reach a chat OTHER than the one you are answering. In a normal chat turn the channel " +
|
|
65
|
+
"already delivers your reply, so do NOT call this to answer (it would send it twice). chatId comes " +
|
|
66
|
+
"from the [telegram: chat …] context line in a chat turn; a scheduled/woken turn has no context " +
|
|
67
|
+
"line, so name the destination in your instruction.",
|
|
65
68
|
input: z.object({
|
|
66
69
|
chatId: z.union([z.string(), z.number()]).describe("target chat id"),
|
|
67
70
|
text: z.string().optional().describe("message text to send"),
|
|
@@ -109,7 +109,7 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
|
|
|
109
109
|
throw new Error(`telegramChannel requires an absolute ctx.stateRoot, got "${stateRoot}"`);
|
|
110
110
|
}
|
|
111
111
|
const stateHome = join(stateRoot, "channels", "telegram");
|
|
112
|
-
ensureStateHome(stateHome); //
|
|
112
|
+
ensureStateHome(stateHome); // buffers/files may carry chat content; the agent .gitignore covers .state/
|
|
113
113
|
const buffer = createContextBuffer(join(stateHome, "buffers.json"));
|
|
114
114
|
// Durable turn intent (L1): persist an accepted turn pre-ACK, remove it when the turn ends; a crash
|
|
115
115
|
// leaves it for replay on the next start. See turn-store.ts for the at-least-once semantics.
|
package/dist/channels/text.d.ts
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
/** Pure Unicode-safe text slicing helpers shared by channel rendering paths (Feishu/Lark cards, the preview kit). JavaScript string
|
|
2
2
|
* indexes are UTF-16 code units, so direct `slice()` can tear a surrogate pair and send replacement
|
|
3
3
|
* characters after JSON/UTF-8 encoding. These helpers only cut at Unicode code-point boundaries. */
|
|
4
|
+
/**
|
|
5
|
+
* How much of a replied-to message is quoted back into a prompt.
|
|
6
|
+
*
|
|
7
|
+
* A referent is the exact text the asker is pointing AT, not a summary of it, so the bound is a
|
|
8
|
+
* fidelity bound: it must clear the largest message a chat platform will accept (Telegram's 4096 is
|
|
9
|
+
* the tightest of ours) or a perfectly legal message loses its tail and the agent answers about text
|
|
10
|
+
* it cannot see — silently. Past that point it is only a guard against a pathological message on a
|
|
11
|
+
* platform with no practical cap. One constant for every channel: the failure this replaces was two
|
|
12
|
+
* channels picking their own number and drifting 14x apart.
|
|
13
|
+
*
|
|
14
|
+
* Distinct from a context-buffer line (see BUFFER_LINE_MAX_CHARS), which is a digest competing for a
|
|
15
|
+
* shared budget — different job, different unit, must not share a number.
|
|
16
|
+
*/
|
|
17
|
+
export declare const REFERENT_MAX_CODE_POINTS = 4096;
|
|
4
18
|
/** Take at most `maxPoints` Unicode code points from the start, without adding a marker. */
|
|
5
19
|
export declare function codePointPrefix(text: string, maxPoints: number): string;
|
|
6
20
|
/** Ellipsize from the right while keeping the result within `maxPoints` Unicode code points. */
|
package/dist/channels/text.js
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
/** Pure Unicode-safe text slicing helpers shared by channel rendering paths (Feishu/Lark cards, the preview kit). JavaScript string
|
|
2
2
|
* indexes are UTF-16 code units, so direct `slice()` can tear a surrogate pair and send replacement
|
|
3
3
|
* characters after JSON/UTF-8 encoding. These helpers only cut at Unicode code-point boundaries. */
|
|
4
|
+
/**
|
|
5
|
+
* How much of a replied-to message is quoted back into a prompt.
|
|
6
|
+
*
|
|
7
|
+
* A referent is the exact text the asker is pointing AT, not a summary of it, so the bound is a
|
|
8
|
+
* fidelity bound: it must clear the largest message a chat platform will accept (Telegram's 4096 is
|
|
9
|
+
* the tightest of ours) or a perfectly legal message loses its tail and the agent answers about text
|
|
10
|
+
* it cannot see — silently. Past that point it is only a guard against a pathological message on a
|
|
11
|
+
* platform with no practical cap. One constant for every channel: the failure this replaces was two
|
|
12
|
+
* channels picking their own number and drifting 14x apart.
|
|
13
|
+
*
|
|
14
|
+
* Distinct from a context-buffer line (see BUFFER_LINE_MAX_CHARS), which is a digest competing for a
|
|
15
|
+
* shared budget — different job, different unit, must not share a number.
|
|
16
|
+
*/
|
|
17
|
+
export const REFERENT_MAX_CODE_POINTS = 4096;
|
|
4
18
|
/** Take at most `maxPoints` Unicode code points from the start, without adding a marker. */
|
|
5
19
|
export function codePointPrefix(text, maxPoints) {
|
|
6
20
|
if (maxPoints <= 0)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface ThreadParticipants {
|
|
2
|
+
/**
|
|
3
|
+
* The participant model's summon rule (docs/design/participant-model.md §3): does a bare message in
|
|
4
|
+
* this thread address the agent? True where it takes part AND no second human has been heard. Lives
|
|
5
|
+
* here rather than in each channel because it is one rule over one store — `<= 1` is an easy edge to
|
|
6
|
+
* get wrong twice, and "a second human restores the mention requirement" must have one place to change.
|
|
7
|
+
*/
|
|
8
|
+
admitsBareMessage(key: string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Merge in what was just heard. Idempotent; a failed write is a warning, never a failed delivery.
|
|
11
|
+
*
|
|
12
|
+
* The parameter only admits values the store can honour: observations accumulate, so `agentSpoke`
|
|
13
|
+
* can be set but never cleared and `humans` unions. Passing `false` would compile and do nothing,
|
|
14
|
+
* so the type refuses it — "never shed" is an invariant, not a convention.
|
|
15
|
+
*/
|
|
16
|
+
merge(key: string, heard: {
|
|
17
|
+
humans?: string[];
|
|
18
|
+
agentSpoke?: true;
|
|
19
|
+
}): void;
|
|
20
|
+
}
|
|
21
|
+
export declare function createThreadParticipants(path: string, label: string): ThreadParticipants;
|