@fastagent-sh/fastagent 0.21.1 → 0.22.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.
- package/README.md +9 -12
- package/dist/agent.d.ts +24 -37
- package/dist/agent.js +7 -15
- package/dist/atomic-write.d.ts +13 -17
- package/dist/atomic-write.js +18 -29
- package/dist/bind.d.ts +12 -22
- package/dist/bind.js +18 -27
- package/dist/channel.d.ts +11 -33
- package/dist/channels/agentcore-limits.d.ts +2 -13
- package/dist/channels/agentcore-limits.js +2 -13
- package/dist/channels/agentcore-protocol.d.ts +67 -58
- package/dist/channels/agentcore-protocol.js +23 -15
- package/dist/channels/agentcore-service.d.ts +19 -33
- package/dist/channels/agentcore-service.js +176 -59
- package/dist/channels/agentcore.d.ts +39 -33
- package/dist/channels/agentcore.js +90 -224
- package/dist/channels/body.d.ts +15 -2
- package/dist/channels/body.js +24 -2
- package/dist/channels/busy.d.ts +1 -21
- package/dist/channels/busy.js +2 -39
- package/dist/channels/control.d.ts +17 -62
- package/dist/channels/control.js +74 -182
- package/dist/channels/define-channel.d.ts +39 -0
- package/dist/channels/define-channel.js +9 -0
- package/dist/channels/discover.d.ts +10 -31
- package/dist/channels/discover.js +37 -31
- package/dist/channels/feishu/bootstrap-token.d.ts +1 -8
- package/dist/channels/feishu/bootstrap-token.js +7 -23
- package/dist/channels/feishu/card.d.ts +14 -28
- package/dist/channels/feishu/card.js +16 -30
- package/dist/channels/feishu/cloud.d.ts +1 -5
- package/dist/channels/feishu/context-buffer.d.ts +4 -23
- package/dist/channels/feishu/context-buffer.js +8 -31
- package/dist/channels/feishu/crypto.d.ts +10 -5
- package/dist/channels/feishu/crypto.js +12 -19
- package/dist/channels/feishu/feishu-api.d.ts +2 -1
- package/dist/channels/feishu/feishu-api.js +5 -4
- package/dist/channels/feishu/feishu.d.ts +4 -10
- package/dist/channels/feishu/feishu.js +113 -227
- package/dist/channels/feishu/invoke-turn.d.ts +5 -4
- package/dist/channels/feishu/invoke-turn.js +36 -54
- package/dist/channels/feishu/model.d.ts +4 -21
- package/dist/channels/feishu/model.js +1 -8
- package/dist/channels/feishu/normalize.d.ts +2 -10
- package/dist/channels/feishu/normalize.js +7 -42
- package/dist/channels/feishu/parse.d.ts +9 -29
- package/dist/channels/feishu/parse.js +8 -26
- package/dist/channels/feishu/preview.d.ts +12 -14
- package/dist/channels/feishu/preview.js +108 -207
- package/dist/channels/feishu/register-app.d.ts +8 -23
- package/dist/channels/feishu/register-app.js +7 -20
- package/dist/channels/feishu/register-webhook.d.ts +2 -9
- package/dist/channels/feishu/register-webhook.js +9 -33
- package/dist/channels/feishu/scaffold/channel.ts +27 -19
- package/dist/channels/feishu/scaffold/channel.websocket.ts +31 -0
- package/dist/channels/feishu/setup-mode.d.ts +11 -36
- package/dist/channels/feishu/setup-mode.js +7 -26
- package/dist/channels/feishu/ws-ingress.d.ts +1 -2
- package/dist/channels/feishu/ws-ingress.js +6 -8
- package/dist/channels/http.d.ts +15 -15
- package/dist/channels/http.js +19 -9
- package/dist/channels/kit/attachment-path.d.ts +4 -5
- package/dist/channels/kit/attachment-path.js +10 -34
- package/dist/channels/kit/context-buffer.d.ts +14 -21
- package/dist/channels/kit/context-buffer.js +25 -38
- package/dist/channels/kit/delivery.d.ts +35 -0
- package/dist/channels/kit/delivery.js +129 -0
- package/dist/channels/kit/event-stream.d.ts +5 -0
- package/dist/channels/kit/event-stream.js +29 -0
- package/dist/channels/kit/invoke-turn-kit.d.ts +53 -26
- package/dist/channels/kit/invoke-turn-kit.js +89 -32
- package/dist/channels/kit/preview-kit.d.ts +18 -72
- package/dist/channels/kit/preview-kit.js +22 -88
- package/dist/channels/kit/signature.d.ts +3 -9
- package/dist/channels/kit/signature.js +3 -9
- package/dist/channels/kit/state.d.ts +1 -2
- package/dist/channels/kit/state.js +5 -14
- package/dist/channels/kit/stop-command.d.ts +1 -2
- package/dist/channels/kit/stop-command.js +6 -9
- package/dist/channels/kit/tasks.d.ts +1 -1
- package/dist/channels/kit/tasks.js +8 -21
- package/dist/channels/kit/text.d.ts +2 -16
- package/dist/channels/kit/text.js +23 -31
- package/dist/channels/kit/thread-participants.d.ts +4 -17
- package/dist/channels/kit/thread-participants.js +8 -55
- package/dist/channels/kit/transport.d.ts +26 -0
- package/dist/channels/kit/transport.js +20 -0
- package/dist/channels/kit/turn-queue.d.ts +7 -11
- package/dist/channels/kit/turn-queue.js +21 -39
- package/dist/channels/kit/turn-runner.d.ts +32 -18
- package/dist/channels/kit/turn-runner.js +98 -53
- package/dist/channels/kit/turn-store.d.ts +32 -32
- package/dist/channels/kit/turn-store.js +45 -88
- package/dist/channels/lark/lark.d.ts +1 -5
- package/dist/channels/lark/onboard.d.ts +3 -12
- package/dist/channels/lark/onboard.js +3 -13
- package/dist/channels/lark/scaffold/channel.ts +27 -19
- package/dist/channels/lark/scaffold/channel.websocket.ts +31 -0
- package/dist/channels/registration.d.ts +9 -35
- package/dist/channels/registration.js +6 -37
- package/dist/channels/secret.js +2 -5
- package/dist/channels/serve.d.ts +47 -44
- package/dist/channels/serve.js +165 -101
- package/dist/channels/slack/config-api.d.ts +4 -27
- package/dist/channels/slack/config-api.js +5 -34
- package/dist/channels/slack/context-buffer.d.ts +2 -3
- package/dist/channels/slack/context-buffer.js +5 -5
- package/dist/channels/slack/invoke-turn.d.ts +3 -1
- package/dist/channels/slack/invoke-turn.js +25 -34
- package/dist/channels/slack/manifest.js +1 -3
- package/dist/channels/slack/model.d.ts +1 -2
- package/dist/channels/slack/model.js +1 -1
- package/dist/channels/slack/onboard.d.ts +4 -3
- package/dist/channels/slack/onboard.js +8 -11
- package/dist/channels/slack/onboarding-state.d.ts +3 -5
- package/dist/channels/slack/onboarding-state.js +9 -6
- package/dist/channels/slack/parse.d.ts +4 -13
- package/dist/channels/slack/parse.js +7 -28
- package/dist/channels/slack/preview.d.ts +12 -2
- package/dist/channels/slack/preview.js +292 -269
- package/dist/channels/slack/reaction.d.ts +11 -3
- package/dist/channels/slack/reaction.js +19 -8
- package/dist/channels/slack/register-webhook.js +4 -7
- package/dist/channels/slack/scaffold/channel.ts +20 -13
- package/dist/channels/slack/scaffold/slack-send.ts +2 -2
- package/dist/channels/slack/setup-server.d.ts +1 -5
- package/dist/channels/slack/setup-server.js +1 -5
- package/dist/channels/slack/shared-api.d.ts +1 -4
- package/dist/channels/slack/shared-api.js +2 -13
- package/dist/channels/slack/slack-api.d.ts +2 -1
- package/dist/channels/slack/slack-api.js +6 -4
- package/dist/channels/slack/slack.d.ts +11 -14
- package/dist/channels/slack/slack.js +121 -122
- package/dist/channels/sse.d.ts +1 -1
- package/dist/channels/sse.js +1 -1
- package/dist/channels/telegram/context-buffer.d.ts +7 -22
- package/dist/channels/telegram/context-buffer.js +6 -23
- package/dist/channels/telegram/invoke-turn.d.ts +7 -7
- package/dist/channels/telegram/invoke-turn.js +24 -43
- package/dist/channels/telegram/parse.d.ts +24 -51
- package/dist/channels/telegram/parse.js +32 -84
- package/dist/channels/telegram/preview.d.ts +10 -1
- package/dist/channels/telegram/preview.js +79 -100
- package/dist/channels/telegram/register-webhook.js +5 -23
- package/dist/channels/telegram/scaffold/channel.ts +25 -18
- package/dist/channels/telegram/scaffold/telegram-send.ts +6 -4
- package/dist/channels/telegram/telegram-api.d.ts +4 -3
- package/dist/channels/telegram/telegram-api.js +9 -7
- package/dist/channels/telegram/telegram.d.ts +4 -18
- package/dist/channels/telegram/telegram.js +38 -94
- package/dist/channels/telegram/turn-store.d.ts +11 -14
- package/dist/channels/telegram/turn-store.js +8 -6
- package/dist/channels/wait-health.d.ts +4 -6
- package/dist/channels/wait-health.js +10 -14
- package/dist/cli/add-feishu.d.ts +2 -12
- package/dist/cli/add-feishu.js +24 -71
- package/dist/cli/add-slack.d.ts +5 -3
- package/dist/cli/add-slack.js +53 -76
- package/dist/cli/auth-view.d.ts +2 -16
- package/dist/cli/auth-view.js +2 -16
- package/dist/cli/commands/add.js +25 -40
- package/dist/cli/commands/chat.d.ts +0 -1
- package/dist/cli/commands/chat.js +5 -6
- package/dist/cli/commands/deploy/agentcore.js +60 -65
- package/dist/cli/commands/deploy/docker.js +50 -23
- package/dist/cli/commands/deploy/fly.js +23 -61
- package/dist/cli/commands/deploy/railway.js +23 -18
- package/dist/cli/commands/deploy/shared.d.ts +38 -57
- package/dist/cli/commands/deploy/shared.js +40 -59
- package/dist/cli/commands/deploy.d.ts +10 -27
- package/dist/cli/commands/deploy.js +38 -60
- package/dist/cli/commands/destroy.d.ts +4 -0
- package/dist/cli/commands/destroy.js +56 -0
- package/dist/cli/commands/dev.d.ts +2 -1
- package/dist/cli/commands/dev.js +20 -23
- package/dist/cli/commands/info.d.ts +0 -2
- package/dist/cli/commands/info.js +83 -49
- package/dist/cli/commands/init.d.ts +1 -2
- package/dist/cli/commands/init.js +16 -55
- package/dist/cli/commands/invoke.d.ts +0 -1
- package/dist/cli/commands/invoke.js +5 -7
- package/dist/cli/commands/login.d.ts +2 -1
- package/dist/cli/commands/login.js +33 -38
- package/dist/cli/commands/logs.js +3 -4
- package/dist/cli/commands/models.js +1 -5
- package/dist/cli/commands/routine-run.d.ts +6 -0
- package/dist/cli/commands/routine-run.js +47 -0
- package/dist/cli/commands/routine.d.ts +25 -0
- package/dist/cli/commands/routine.js +122 -0
- package/dist/cli/commands/start.d.ts +31 -3
- package/dist/cli/commands/start.js +131 -81
- package/dist/cli/commands/tool.js +26 -12
- package/dist/cli/fail.d.ts +13 -23
- package/dist/cli/fail.js +20 -23
- package/dist/cli/invoke-stream.d.ts +1 -8
- package/dist/cli/kernel.d.ts +6 -38
- package/dist/cli/kernel.js +14 -77
- package/dist/cli/models-view.d.ts +1 -9
- package/dist/cli/models-view.js +11 -22
- package/dist/cli/program.d.ts +5 -6
- package/dist/cli/program.js +120 -157
- package/dist/cli/serve.d.ts +57 -41
- package/dist/cli/serve.js +94 -121
- package/dist/cli/shared.d.ts +15 -61
- package/dist/cli/shared.js +55 -181
- package/dist/cli.js +1 -8
- package/dist/collect.d.ts +6 -0
- package/dist/collect.js +6 -0
- package/dist/core.d.ts +4 -1
- package/dist/core.js +9 -7
- package/dist/declared-secrets.d.ts +55 -0
- package/dist/declared-secrets.js +88 -0
- package/dist/deploy/agentcore/aws-cli.d.ts +91 -0
- package/dist/deploy/agentcore/aws-cli.js +64 -0
- package/dist/deploy/agentcore/destroy.d.ts +51 -0
- package/dist/deploy/agentcore/destroy.js +273 -0
- package/dist/deploy/agentcore/forwarder.js +28 -79
- package/dist/deploy/agentcore/logs.d.ts +2 -18
- package/dist/deploy/agentcore/logs.js +27 -51
- package/dist/deploy/agentcore/plan.d.ts +85 -105
- package/dist/deploy/agentcore/plan.js +152 -314
- package/dist/deploy/agentcore/run.d.ts +28 -28
- package/dist/deploy/agentcore/run.js +227 -306
- package/dist/deploy/agentcore/zip.d.ts +2 -15
- package/dist/deploy/agentcore/zip.js +3 -17
- package/dist/deploy/build-context.d.ts +21 -0
- package/dist/deploy/build-context.js +137 -0
- package/dist/deploy/channel-ingress.d.ts +26 -39
- package/dist/deploy/channel-ingress.js +57 -28
- package/dist/deploy/container.d.ts +20 -34
- package/dist/deploy/container.js +49 -100
- package/dist/deploy/docker/plan.d.ts +12 -11
- package/dist/deploy/docker/plan.js +43 -24
- package/dist/deploy/docker/run.d.ts +22 -25
- package/dist/deploy/docker/run.js +39 -37
- package/dist/deploy/fly/plan.d.ts +15 -44
- package/dist/deploy/fly/plan.js +41 -73
- package/dist/deploy/fly/run.d.ts +16 -51
- package/dist/deploy/fly/run.js +42 -95
- package/dist/deploy/hosts.d.ts +24 -3
- package/dist/deploy/hosts.js +24 -3
- package/dist/deploy/preflight.d.ts +47 -41
- package/dist/deploy/preflight.js +282 -383
- package/dist/deploy/railway/plan.d.ts +16 -42
- package/dist/deploy/railway/plan.js +42 -60
- package/dist/deploy/railway/run.d.ts +30 -68
- package/dist/deploy/railway/run.js +86 -100
- package/dist/deploy/registration-gate.d.ts +6 -13
- package/dist/deploy/residency.d.ts +37 -0
- package/dist/deploy/residency.js +24 -0
- package/dist/deploy/runner.d.ts +10 -9
- package/dist/deploy/runner.js +7 -18
- package/dist/deploy/secrets.d.ts +40 -41
- package/dist/deploy/secrets.js +117 -103
- package/dist/deploy/workspace.d.ts +50 -0
- package/dist/deploy/workspace.js +197 -0
- package/dist/dev-supervisor.d.ts +1 -9
- package/dist/dev-supervisor.js +21 -64
- package/dist/effect-port.d.ts +50 -0
- package/dist/effect-port.js +77 -0
- package/dist/engines/pi/agent-session-factory.d.ts +50 -69
- package/dist/engines/pi/agent-session-factory.js +249 -146
- package/dist/engines/pi/auth.d.ts +15 -11
- package/dist/engines/pi/auth.js +96 -99
- package/dist/engines/pi/chat.d.ts +1 -5
- package/dist/engines/pi/chat.js +2 -14
- package/dist/engines/pi/config.d.ts +53 -51
- package/dist/engines/pi/config.js +83 -92
- package/dist/engines/pi/create.d.ts +60 -75
- package/dist/engines/pi/create.js +108 -141
- package/dist/engines/pi/definition.d.ts +14 -58
- package/dist/engines/pi/definition.js +21 -85
- package/dist/engines/pi/invoke-session.d.ts +5 -25
- package/dist/engines/pi/invoke-session.js +209 -244
- package/dist/engines/pi/login.d.ts +95 -35
- package/dist/engines/pi/login.js +259 -107
- package/dist/engines/pi/machine.d.ts +31 -0
- package/dist/engines/pi/machine.js +106 -0
- package/dist/engines/pi/models.d.ts +70 -89
- package/dist/engines/pi/models.js +188 -81
- package/dist/engines/pi/open.d.ts +65 -53
- package/dist/engines/pi/open.js +129 -69
- package/dist/engines/pi/report.d.ts +3 -12
- package/dist/engines/pi/report.js +2 -16
- package/dist/engines/pi/search-tools.d.ts +1 -9
- package/dist/engines/pi/search-tools.js +26 -57
- package/dist/engines/pi/service.d.ts +2 -16
- package/dist/engines/pi/service.js +2 -16
- package/dist/engines/pi/session-builder.d.ts +5 -9
- package/dist/engines/pi/session-builder.js +44 -82
- package/dist/engines/pi/session-control.js +286 -269
- package/dist/engines/pi/session-effects.d.ts +14 -0
- package/dist/engines/pi/session-effects.js +30 -0
- package/dist/engines/pi/session-inheritance.d.ts +9 -18
- package/dist/engines/pi/session-inheritance.js +103 -160
- package/dist/engines/pi/session-markers.d.ts +31 -30
- package/dist/engines/pi/session-markers.js +26 -25
- package/dist/engines/pi/session-settings.d.ts +6 -22
- package/dist/engines/pi/session-settings.js +10 -21
- package/dist/engines/pi/session-store.d.ts +26 -92
- package/dist/engines/pi/session-store.js +96 -310
- package/dist/engines/pi/tool-context.d.ts +17 -45
- package/dist/engines/pi/tool-context.js +7 -26
- package/dist/engines/pi/tool.d.ts +45 -36
- package/dist/engines/pi/tool.js +43 -57
- package/dist/engines/pi/turn-kit.d.ts +14 -52
- package/dist/engines/pi/turn-kit.js +18 -67
- package/dist/engines/pi/wake-tool.d.ts +2 -10
- package/dist/engines/pi/wake-tool.js +7 -27
- package/dist/env.d.ts +27 -28
- package/dist/env.js +45 -50
- package/dist/index.js +0 -2
- package/dist/lark.d.ts +1 -2
- package/dist/lark.js +1 -2
- package/dist/loader.d.ts +14 -55
- package/dist/loader.js +24 -43
- package/dist/log.d.ts +2 -12
- package/dist/log.js +3 -24
- package/dist/node.d.ts +2 -15
- package/dist/node.js +2 -18
- package/dist/observe.d.ts +2 -8
- package/dist/open-url.d.ts +1 -1
- package/dist/open-url.js +1 -1
- package/dist/paths.d.ts +70 -134
- package/dist/paths.js +165 -232
- package/dist/pi.d.ts +4 -6
- package/dist/pi.js +3 -3
- package/dist/proxy.d.ts +16 -6
- package/dist/proxy.js +52 -16
- package/dist/runtime.d.ts +18 -12
- package/dist/runtime.js +18 -11
- package/dist/scaffold/add-channel.d.ts +14 -31
- package/dist/scaffold/add-channel.js +48 -112
- package/dist/scaffold/init.d.ts +9 -31
- package/dist/scaffold/init.js +45 -138
- package/dist/scaffold/templates/env.example +6 -4
- package/dist/scaffold/templates/fastagent.config.ts +36 -0
- package/dist/scaffold/templates.d.ts +3 -3
- package/dist/scaffold/templates.js +5 -8
- package/dist/scaffold/vendor-skill.d.ts +2 -5
- package/dist/scaffold/vendor-skill.js +19 -29
- package/dist/schedule/cron.d.ts +9 -5
- package/dist/schedule/cron.js +9 -11
- package/dist/schedule/discover.d.ts +12 -12
- package/dist/schedule/discover.js +43 -34
- package/dist/schedule/routine.d.ts +43 -0
- package/dist/schedule/routine.js +32 -0
- package/dist/schedule/run.d.ts +55 -0
- package/dist/schedule/run.js +146 -0
- package/dist/schedule/scheduler.d.ts +44 -55
- package/dist/schedule/scheduler.js +291 -265
- package/dist/schedule/state.d.ts +105 -10
- package/dist/schedule/state.js +293 -19
- package/dist/schedule/wake-alarm.d.ts +10 -23
- package/dist/schedule/wake-alarm.js +91 -121
- package/dist/schedule/wakeups.d.ts +23 -39
- package/dist/schedule/wakeups.js +33 -75
- package/dist/secrets-gate.d.ts +32 -0
- package/dist/secrets-gate.js +33 -0
- package/dist/service.d.ts +137 -98
- package/dist/service.js +250 -252
- package/dist/session-remote.d.ts +33 -44
- package/dist/session-remote.js +203 -192
- package/dist/session.d.ts +153 -228
- package/dist/session.js +33 -55
- package/dist/tunnel.d.ts +8 -36
- package/dist/tunnel.js +24 -65
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/package.json +10 -16
- package/dist/channels/agentcore-state.d.ts +0 -77
- package/dist/channels/agentcore-state.js +0 -276
- package/dist/channels/github/github.d.ts +0 -31
- package/dist/channels/github/github.js +0 -89
- package/dist/channels/github/scaffold/channel.ts +0 -25
- package/dist/cli/commands/attach.d.ts +0 -108
- package/dist/cli/commands/attach.js +0 -637
- package/dist/cli/commands/fire.d.ts +0 -7
- package/dist/cli/commands/fire.js +0 -38
- package/dist/cli/commands/schedule.d.ts +0 -12
- package/dist/cli/commands/schedule.js +0 -87
- package/dist/github.d.ts +0 -3
- package/dist/github.js +0 -2
- package/dist/scaffold/templates/fastagent.config.mjs +0 -23
- package/dist/schedule/audit.d.ts +0 -18
- package/dist/schedule/audit.js +0 -57
- package/dist/schedule/schedule.d.ts +0 -35
- package/dist/schedule/schedule.js +0 -23
|
@@ -1,28 +1,21 @@
|
|
|
1
1
|
import { truncateCodePointPrefix, truncateCodePointSuffix } from "./text.js";
|
|
2
|
-
/** The customer-facing default: neutral, no leaked internals.
|
|
3
|
-
* offer a next step (Slack agent-design: never leave the user with a dead-end "something went wrong").
|
|
4
|
-
* The non-retryable branch keeps the "something went wrong" phrase deliberately — it is neutral (we only
|
|
5
|
-
* know a boolean, never the specific limitation) and shared verbatim across channels. */
|
|
2
|
+
/** The customer-facing default: neutral, no leaked internals. */
|
|
6
3
|
export function defaultErrorMessage(failed) {
|
|
7
4
|
return failed.retryable
|
|
8
5
|
? "⚠️ Temporary problem — please try again in a moment."
|
|
9
6
|
: "⚠️ Sorry, something went wrong. Try rephrasing, or check I have access to what you need.";
|
|
10
7
|
}
|
|
11
|
-
/**
|
|
12
|
-
*
|
|
8
|
+
/**
|
|
9
|
+
* Customer-facing live-preview line for an engine-internal retry backoff (the advisory `retrying` event): neutral, no
|
|
10
|
+
* leaked internals — the reason stays in operator logs.
|
|
11
|
+
*/
|
|
13
12
|
export const RETRY_NOTICE = "⏳ Temporary problem — retrying…";
|
|
14
13
|
/** The placeholder shown before any reasoning/tool/text arrives. */
|
|
15
14
|
export const THINKING_PLACEHOLDER = "💭 Thinking…";
|
|
16
15
|
export function createTurnView() {
|
|
17
16
|
return { thinking: "", tools: [], toolById: new Map(), answer: "", retrying: false };
|
|
18
17
|
}
|
|
19
|
-
|
|
20
|
-
* Apply one event to the view state. Returns true when the view changed (the caller repaints).
|
|
21
|
-
* This is the ONE place the shared view rules live: tool labels are humanized with a compact arg
|
|
22
|
-
* summary, and any progress event closes an open retry notice (a stale "retrying" line must never
|
|
23
|
-
* outlive actual progress). Terminal events only close the notice — they are the caller's business.
|
|
24
|
-
*/
|
|
25
|
-
export function applyTurnEvent(view, e) {
|
|
18
|
+
export function applyTurnEvent(view, e, now = Date.now()) {
|
|
26
19
|
const closedRetry = view.retrying && e.type !== "retrying";
|
|
27
20
|
if (closedRetry)
|
|
28
21
|
view.retrying = false;
|
|
@@ -30,7 +23,7 @@ export function applyTurnEvent(view, e) {
|
|
|
30
23
|
case "text":
|
|
31
24
|
view.answer += e.delta;
|
|
32
25
|
if (view.answerSince === undefined && view.answer.trim() !== "")
|
|
33
|
-
view.answerSince =
|
|
26
|
+
view.answerSince = now;
|
|
34
27
|
return true;
|
|
35
28
|
case "thinking":
|
|
36
29
|
view.thinking += e.delta;
|
|
@@ -61,25 +54,19 @@ const TOOL_MARK = { running: "…", ok: "✓", error: "✗" };
|
|
|
61
54
|
export function toolLines(view) {
|
|
62
55
|
return view.tools.map((t) => `🔧 ${t.label} ${TOOL_MARK[t.status]}`).join("\n");
|
|
63
56
|
}
|
|
64
|
-
/** The reasoning peek: the most recent tail of the (growing) reasoning, one line, code-point safe.
|
|
65
|
-
* Process, not the answer — renderers show it live only, never in the persisted final message. */
|
|
57
|
+
/** The reasoning peek: the most recent tail of the (growing) reasoning, one line, code-point safe. */
|
|
66
58
|
export function thinkingLine(view, maxTail) {
|
|
67
59
|
const t = view.thinking.replace(/\s+/g, " ").trim();
|
|
68
60
|
return t === "" ? "" : `💭 ${truncateCodePointSuffix(t, maxTail)}`;
|
|
69
61
|
}
|
|
70
62
|
/**
|
|
71
|
-
* The shared answer-reveal policy: the answer stays hidden until its first delta has aged one
|
|
72
|
-
*
|
|
73
|
-
* flush would otherwise turn the very first content delta (often a lone character or unbalanced
|
|
74
|
-
* markup) into its own frame — the short-reply flicker (placeholder → "O" → "OK."). Aging is
|
|
75
|
-
* anchored at delta ARRIVAL (`answerSince`, set by the reducer) so an in-flight write can't skew the
|
|
76
|
-
* clock, and there is deliberately NO timer at the boundary: a young answer surfaces on the next
|
|
77
|
-
* content-driven pass, so a turn completing within the window delivers the final answer only.
|
|
63
|
+
* The shared answer-reveal policy: the answer stays hidden until its first delta has aged one throttle window (`ageMs`
|
|
64
|
+
* — each platform passes its own pacing constant).
|
|
78
65
|
*/
|
|
79
|
-
export function revealedAnswer(view, ageMs) {
|
|
66
|
+
export function revealedAnswer(view, ageMs, now = Date.now()) {
|
|
80
67
|
if (view.answer.trim() === "" || view.answerSince === undefined)
|
|
81
68
|
return "";
|
|
82
|
-
return
|
|
69
|
+
return now - view.answerSince >= ageMs ? view.answer : "";
|
|
83
70
|
}
|
|
84
71
|
/** Compose body parts (thinking/tools/retry/answer) into one frame: skip empties, blank-line joins. */
|
|
85
72
|
export function composeTurnBody(parts) {
|
|
@@ -88,71 +75,21 @@ export function composeTurnBody(parts) {
|
|
|
88
75
|
.join("\n\n")
|
|
89
76
|
.trim();
|
|
90
77
|
}
|
|
91
|
-
|
|
92
|
-
let dirty = false;
|
|
93
|
-
let pumping = false;
|
|
94
|
-
let stopped = false;
|
|
95
|
-
let errored = false;
|
|
96
|
-
let pumpDone;
|
|
97
|
-
let wakeThrottle; // set while mid-throttle; finish() cuts it short
|
|
98
|
-
const runPump = async () => {
|
|
99
|
-
pumping = true;
|
|
100
|
-
try {
|
|
101
|
-
while (dirty && !stopped) {
|
|
102
|
-
dirty = false;
|
|
103
|
-
try {
|
|
104
|
-
await opts.flush();
|
|
105
|
-
}
|
|
106
|
-
catch (error) {
|
|
107
|
-
if (!errored) {
|
|
108
|
-
errored = true;
|
|
109
|
-
opts.onError(error);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
if (dirty && !stopped) {
|
|
113
|
-
await new Promise((resolve) => {
|
|
114
|
-
const t = setTimeout(resolve, opts.throttleMs);
|
|
115
|
-
wakeThrottle = () => {
|
|
116
|
-
clearTimeout(t);
|
|
117
|
-
resolve();
|
|
118
|
-
};
|
|
119
|
-
});
|
|
120
|
-
wakeThrottle = undefined;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
finally {
|
|
125
|
-
pumping = false;
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
return {
|
|
129
|
-
touch() {
|
|
130
|
-
dirty = true;
|
|
131
|
-
if (!pumping)
|
|
132
|
-
pumpDone = runPump();
|
|
133
|
-
},
|
|
134
|
-
async finish() {
|
|
135
|
-
stopped = true;
|
|
136
|
-
wakeThrottle?.();
|
|
137
|
-
await pumpDone?.catch(() => { });
|
|
138
|
-
},
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
/** Max length (code points) of a tool's arg preview. Slack's native stream cannot be retracted, so
|
|
142
|
-
* this is a disclosure bound, not only a layout one. */
|
|
78
|
+
/** Max length (code points) of a tool's arg preview. */
|
|
143
79
|
const TOOL_ARG_MAX = 48;
|
|
144
80
|
/** Max length (code points) of a humanized tool label. */
|
|
145
81
|
const TOOL_NAME_MAX = 80;
|
|
146
|
-
/**
|
|
147
|
-
*
|
|
82
|
+
/**
|
|
83
|
+
* One-line, truncated at code-point boundaries: collapse whitespace so a multi-line command/arg stays on one line, and
|
|
84
|
+
* never tear a surrogate pair mid-emoji.
|
|
85
|
+
*/
|
|
148
86
|
function clip(s) {
|
|
149
87
|
const one = s.replace(/\s+/g, " ").trim();
|
|
150
88
|
return truncateCodePointPrefix(one, TOOL_ARG_MAX);
|
|
151
89
|
}
|
|
152
90
|
/**
|
|
153
|
-
* A compact, human-readable preview of a tool call's args so the live view reads `🔧 read AGENTS.md`
|
|
154
|
-
*
|
|
155
|
-
* first primitive field, conventionally the subject (path / command / query / url) — else compact JSON.
|
|
91
|
+
* A compact, human-readable preview of a tool call's args so the live view reads `🔧 read AGENTS.md` rather than just
|
|
92
|
+
* `🔧 read`.
|
|
156
93
|
*/
|
|
157
94
|
export function summarizeToolArgs(args) {
|
|
158
95
|
if (args === null || typeof args !== "object" || Array.isArray(args))
|
|
@@ -164,12 +101,9 @@ export function summarizeToolArgs(args) {
|
|
|
164
101
|
return values.length > 0 ? clip(JSON.stringify(args)) : "";
|
|
165
102
|
}
|
|
166
103
|
/**
|
|
167
|
-
* A plain-language label for a tool call, following Slack's agent-design guidance to name what a tool
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
* identifier string — it never invents semantics and never exposes arguments. An `mcp__server__tool`
|
|
171
|
-
* identifier becomes `server: tool`; any other identifier has its separators normalized to spaces and
|
|
172
|
-
* its first letter capitalized.
|
|
104
|
+
* A plain-language label for a tool call, following Slack's agent-design guidance to name what a tool does rather than
|
|
105
|
+
* expose a raw identifier ("Create issue", not "create_issue"; "Github: create issue", not
|
|
106
|
+
* "mcp__github__create_issue").
|
|
173
107
|
*/
|
|
174
108
|
export function humanizeToolName(name) {
|
|
175
109
|
const normalize = (s) => s
|
|
@@ -1,12 +1,6 @@
|
|
|
1
|
+
/** The replay window every signed webhook ingress needs. */
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* The window LENGTH is the caller's: it is set by the platform's own redelivery schedule, not by us.
|
|
5
|
-
*/
|
|
6
|
-
/**
|
|
7
|
-
* Whether a Unix-SECONDS timestamp header is within `maxAgeS` of now, in either direction (a clock
|
|
8
|
-
* ahead of ours is as suspect as one behind). Non-numeric is not fresh: the header is part of the
|
|
9
|
-
* signed material, so a value the signature commits to but this cannot read is a reason to refuse,
|
|
10
|
-
* not to wave through.
|
|
3
|
+
* Whether a Unix-SECONDS timestamp header is within `maxAgeS` of now, in either direction (a clock ahead of ours is as
|
|
4
|
+
* suspect as one behind).
|
|
11
5
|
*/
|
|
12
6
|
export declare function signatureIsFresh(timestamp: string, maxAgeS: number, nowMs?: number): boolean;
|
|
@@ -1,13 +1,7 @@
|
|
|
1
|
+
/** The replay window every signed webhook ingress needs. */
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* The window LENGTH is the caller's: it is set by the platform's own redelivery schedule, not by us.
|
|
5
|
-
*/
|
|
6
|
-
/**
|
|
7
|
-
* Whether a Unix-SECONDS timestamp header is within `maxAgeS` of now, in either direction (a clock
|
|
8
|
-
* ahead of ours is as suspect as one behind). Non-numeric is not fresh: the header is part of the
|
|
9
|
-
* signed material, so a value the signature commits to but this cannot read is a reason to refuse,
|
|
10
|
-
* not to wave through.
|
|
3
|
+
* Whether a Unix-SECONDS timestamp header is within `maxAgeS` of now, in either direction (a clock ahead of ours is as
|
|
4
|
+
* suspect as one behind).
|
|
11
5
|
*/
|
|
12
6
|
export function signatureIsFresh(timestamp, maxAgeS, nowMs = Date.now()) {
|
|
13
7
|
if (!/^\d+$/.test(timestamp))
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/** Create the channel's state home — the one shared spelling of it, so no channel invents its own. */
|
|
2
2
|
export declare function ensureStateHome(dir: string): void;
|
|
3
|
-
/** Returns `unknown` on purpose — no generic pretending otherwise
|
|
4
|
-
* caller owns shape validation (a `<T>` here would be an unchecked cast wearing a type). */
|
|
3
|
+
/** Returns `unknown` on purpose — no generic pretending otherwise. */
|
|
5
4
|
export declare function loadStateFile(path: string): unknown;
|
|
6
5
|
export declare function saveStateFile(path: string, value: unknown): void;
|
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Durable channel state
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* is what makes the state actually durable (an ACKed delivery is not redelivered).
|
|
7
|
-
* Channel-neutral: every stateful channel (Telegram, Feishu/Lark, Slack) derives its home from the ctx state root
|
|
8
|
-
* and persists through these three primitives.
|
|
9
|
-
*
|
|
10
|
-
* Failure split: a CORRUPT file (bad JSON) degrades visibly — log.warn + start empty — because channel
|
|
11
|
-
* state is recoverable context, not worth refusing to boot over. An unreadable file (permissions, IO)
|
|
12
|
-
* is an ENVIRONMENT error the operator must fix: it throws, and construction fails loudly — booting
|
|
13
|
-
* with silently-empty state would hide real data behind a config mistake.
|
|
2
|
+
* Durable channel state, with no cross-instance locking: every file here is keyed by CHANNEL KIND
|
|
3
|
+
* (`<stateRoot>/channels/<kind>/`), so what must not happen is two processes serving the SAME channel — which also
|
|
4
|
+
* means one ingress credential in two places, and no local guard can see that. Two processes serving different
|
|
5
|
+
* channels of one agent never touch the same file (docs/design/core.md, "the shipped file-backed implementations").
|
|
14
6
|
*/
|
|
15
7
|
import { mkdirSync, readFileSync } from "node:fs";
|
|
16
8
|
import { writeFileAtomic } from "../../atomic-write.js";
|
|
@@ -19,8 +11,7 @@ import { log } from "../../log.js";
|
|
|
19
11
|
export function ensureStateHome(dir) {
|
|
20
12
|
mkdirSync(dir, { recursive: true });
|
|
21
13
|
}
|
|
22
|
-
/** Returns `unknown` on purpose — no generic pretending otherwise
|
|
23
|
-
* caller owns shape validation (a `<T>` here would be an unchecked cast wearing a type). */
|
|
14
|
+
/** Returns `unknown` on purpose — no generic pretending otherwise. */
|
|
24
15
|
export function loadStateFile(path) {
|
|
25
16
|
let raw;
|
|
26
17
|
try {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { type SessionControl } from "../../session.ts";
|
|
2
2
|
/** Bare stop word for summon-body matching (Slack/Feishu); Telegram uses its native `/stop` command. */
|
|
3
3
|
export declare function isStopText(text: string): boolean;
|
|
4
|
-
/** Dispatch `abort` for the session and map the outcome to the customer-facing line.
|
|
5
|
-
* full details go to the operator log. */
|
|
4
|
+
/** Dispatch `abort` for the session and map the outcome to the customer-facing line. */
|
|
6
5
|
export declare function dispatchStop(control: SessionControl | undefined, session: string, label: string): Promise<string>;
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SHARED user-facing stop command: the chat channels (telegram, feishu/lark, slack) map an explicit
|
|
3
|
-
*
|
|
4
|
-
* §15): the stop message is a CONTROL ACTION, never a turn (it must not queue behind the run it
|
|
5
|
-
* stops); only the ACTIVE run is aborted — queued durable turns are independent asks and keep their
|
|
6
|
-
* at-least-once floor; and the hub stays gated by `config.sessionControl`, so without it the command
|
|
7
|
-
* degrades to a visible "not enabled" notice, never a silent ignore.
|
|
2
|
+
* SHARED user-facing stop command: the chat channels (telegram, feishu/lark, slack) map an explicit "stop" ask onto
|
|
3
|
+
* the session control plane's `abort`.
|
|
8
4
|
*/
|
|
9
5
|
import { log } from "../../log.js";
|
|
10
6
|
import { NO_ACTIVE_RUN_CODE } from "../../session.js";
|
|
@@ -14,9 +10,10 @@ export function isStopText(text) {
|
|
|
14
10
|
}
|
|
15
11
|
const STOPPED_NOTICE = "⏹ Stopped.";
|
|
16
12
|
const NOTHING_RUNNING_NOTICE = "Nothing is running.";
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
// A serve always builds the hub, so this is only reachable when an embedder mounted a service without one — never by
|
|
14
|
+
// a configuration an author can change, which is why it no longer names a setting to turn on.
|
|
15
|
+
const STOP_UNAVAILABLE_NOTICE = "⚠️ Stop isn't available on this deployment.";
|
|
16
|
+
/** Dispatch `abort` for the session and map the outcome to the customer-facing line. */
|
|
20
17
|
export async function dispatchStop(control, session, label) {
|
|
21
18
|
if (!control)
|
|
22
19
|
return STOP_UNAVAILABLE_NOTICE;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export interface TaskTracker {
|
|
2
|
-
/** Track
|
|
2
|
+
/** Track already-started work. Rejections are logged without failing the drain. */
|
|
3
3
|
track(task: Promise<unknown>): void;
|
|
4
4
|
/** Resolves when every currently-tracked task has settled. */
|
|
5
5
|
drain(): Promise<void>;
|
|
@@ -1,31 +1,18 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
* handling stays with the caller: track() only guarantees the drain sees the task SETTLE, and settle
|
|
6
|
-
* includes reject. A caller that handles its error on a separate branch (`p.catch(log); track(p)`)
|
|
7
|
-
* still hands us a promise that rejects, and a drain that propagated it would fail the channel's whole
|
|
8
|
-
* `turnsIdle` over one side task. A rejection that reaches us is logged — we
|
|
9
|
-
* cannot tell a missing `.catch` from one on a separate branch, so the line is a visibility floor
|
|
10
|
-
* rather than a diagnosis, and without it a dropped side task leaves no trace anywhere.
|
|
11
|
-
*/
|
|
1
|
+
/** ACK-independent side-task tracking. Drains are observation hooks, not service shutdown. */
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Fiber from "effect/Fiber";
|
|
4
|
+
import { portError, portJoin } from "../../effect-port.js";
|
|
12
5
|
import { beginWork } from "../busy.js";
|
|
13
6
|
import { log } from "../../log.js";
|
|
14
7
|
export function createTaskTracker(label) {
|
|
15
8
|
const tasks = new Set();
|
|
16
9
|
return {
|
|
17
10
|
track(task) {
|
|
18
|
-
tasks.add(task);
|
|
19
|
-
// Tracked side tasks count as process-wide in-flight work (busy.ts) — same signal the turn
|
|
20
|
-
// queue reports, read by serving surfaces that must not idle while background work runs.
|
|
21
11
|
const workDone = beginWork();
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
tasks.delete(task);
|
|
26
|
-
})
|
|
27
|
-
.catch((error) => log.warn(`${label} side task rejected: ${String(error)}`));
|
|
12
|
+
const fiber = Effect.runFork(portJoin(() => task).pipe(Effect.asVoid, Effect.catchCause((cause) => Effect.sync(() => log.warn(`${label} side task rejected: ${String(portError(cause))}`))), Effect.ensuring(Effect.sync(workDone))));
|
|
13
|
+
tasks.add(fiber);
|
|
14
|
+
fiber.addObserver(() => tasks.delete(fiber));
|
|
28
15
|
},
|
|
29
|
-
drain: () =>
|
|
16
|
+
drain: () => Effect.runPromise(Effect.asVoid(Fiber.awaitAll([...tasks]))),
|
|
30
17
|
};
|
|
31
18
|
}
|
|
@@ -1,19 +1,5 @@
|
|
|
1
|
-
/** Pure Unicode-safe text slicing helpers shared by channel rendering paths (Feishu/Lark cards, the preview kit).
|
|
2
|
-
|
|
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
|
-
*/
|
|
1
|
+
/** Pure Unicode-safe text slicing helpers shared by channel rendering paths (Feishu/Lark cards, the preview kit). */
|
|
2
|
+
/** How much of a replied-to message is quoted back into a prompt. */
|
|
17
3
|
export declare const REFERENT_MAX_CODE_POINTS = 4096;
|
|
18
4
|
/** Take at most `maxPoints` Unicode code points from the start, without adding a marker. */
|
|
19
5
|
export declare function codePointPrefix(text: string, maxPoints: number): string;
|
|
@@ -1,19 +1,5 @@
|
|
|
1
|
-
/** Pure Unicode-safe text slicing helpers shared by channel rendering paths (Feishu/Lark cards, the preview kit).
|
|
2
|
-
|
|
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
|
-
*/
|
|
1
|
+
/** Pure Unicode-safe text slicing helpers shared by channel rendering paths (Feishu/Lark cards, the preview kit). */
|
|
2
|
+
/** How much of a replied-to message is quoted back into a prompt. */
|
|
17
3
|
export const REFERENT_MAX_CODE_POINTS = 4096;
|
|
18
4
|
/** Take at most `maxPoints` Unicode code points from the start, without adding a marker. */
|
|
19
5
|
export function codePointPrefix(text, maxPoints) {
|
|
@@ -27,29 +13,35 @@ export function codePointPrefix(text, maxPoints) {
|
|
|
27
13
|
}
|
|
28
14
|
return out.join("");
|
|
29
15
|
}
|
|
30
|
-
/**
|
|
31
|
-
|
|
16
|
+
/**
|
|
17
|
+
* How much of `text` survives beside `marker`, or the finished answer when the marker alone decides it: nothing to
|
|
18
|
+
* cut, no room at all, or room for less than the marker. Both truncations answer those three the same way, so they
|
|
19
|
+
* are decided once.
|
|
20
|
+
*/
|
|
21
|
+
function budget(text, maxPoints, marker) {
|
|
32
22
|
const points = Array.from(text);
|
|
33
23
|
if (points.length <= maxPoints)
|
|
34
|
-
return text;
|
|
24
|
+
return { done: text };
|
|
35
25
|
if (maxPoints <= 0)
|
|
36
|
-
return "";
|
|
26
|
+
return { done: "" };
|
|
37
27
|
const markerPoints = Array.from(marker);
|
|
38
28
|
if (markerPoints.length >= maxPoints)
|
|
39
|
-
return markerPoints.slice(0, maxPoints).join("");
|
|
40
|
-
return
|
|
29
|
+
return { done: markerPoints.slice(0, maxPoints).join("") };
|
|
30
|
+
return { points, keep: maxPoints - markerPoints.length };
|
|
31
|
+
}
|
|
32
|
+
/** Ellipsize from the right while keeping the result within `maxPoints` Unicode code points. */
|
|
33
|
+
export function truncateCodePointPrefix(text, maxPoints, marker = "…") {
|
|
34
|
+
const fit = budget(text, maxPoints, marker);
|
|
35
|
+
if ("done" in fit)
|
|
36
|
+
return fit.done;
|
|
37
|
+
return `${fit.points.slice(0, fit.keep).join("")}${marker}`;
|
|
41
38
|
}
|
|
42
39
|
/** Ellipsize from the left while keeping the result within `maxPoints` Unicode code points. */
|
|
43
40
|
export function truncateCodePointSuffix(text, maxPoints, marker = "…") {
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
46
|
-
return
|
|
47
|
-
|
|
48
|
-
return "";
|
|
49
|
-
const markerPoints = Array.from(marker);
|
|
50
|
-
if (markerPoints.length >= maxPoints)
|
|
51
|
-
return markerPoints.slice(0, maxPoints).join("");
|
|
52
|
-
return `${marker}${points.slice(points.length - (maxPoints - markerPoints.length)).join("")}`;
|
|
41
|
+
const fit = budget(text, maxPoints, marker);
|
|
42
|
+
if ("done" in fit)
|
|
43
|
+
return fit.done;
|
|
44
|
+
return `${marker}${fit.points.slice(fit.points.length - fit.keep).join("")}`;
|
|
53
45
|
}
|
|
54
46
|
/** Largest code-point-aligned prefix whose UTF-8 encoding fits `maxBytes`. */
|
|
55
47
|
export function utf8Prefix(text, maxBytes) {
|
|
@@ -1,25 +1,12 @@
|
|
|
1
1
|
export interface ThreadParticipants {
|
|
2
2
|
/**
|
|
3
|
-
* The participant model's summon rule (docs/design/participant-model.md §3): does a bare message in
|
|
4
|
-
*
|
|
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.
|
|
3
|
+
* The participant model's summon rule (docs/design/participant-model.md §3): does a bare message in this thread
|
|
4
|
+
* address the agent?
|
|
7
5
|
*/
|
|
8
6
|
admitsBareMessage(key: string): boolean;
|
|
9
|
-
/**
|
|
10
|
-
* Has the agent answered into this thread before — the "first answered turn" fact
|
|
11
|
-
* (participant-model.md §8), unlike {@link ThreadParticipants.admitsBareMessage} which also weighs
|
|
12
|
-
* the second-human rule. An evicted record answers false (this store is a cache — see the header),
|
|
13
|
-
* so gate a repeatable read on it, never a durable claim.
|
|
14
|
-
*/
|
|
7
|
+
/** Has the agent answered into this thread before. */
|
|
15
8
|
agentSpokeIn(key: string): boolean;
|
|
16
|
-
/**
|
|
17
|
-
* Merge in what was just heard. Idempotent; a failed write is a warning, never a failed delivery.
|
|
18
|
-
*
|
|
19
|
-
* The parameter only admits values the store can honour: observations accumulate, so `agentSpoke`
|
|
20
|
-
* can be set but never cleared and `humans` unions. Passing `false` would compile and do nothing,
|
|
21
|
-
* so the type refuses it — "never shed" is an invariant, not a convention.
|
|
22
|
-
*/
|
|
9
|
+
/** `agentSpoke?: true` and not `boolean`: observations only ever accumulate, so there is no "un-speak" to pass. */
|
|
23
10
|
merge(key: string, heard: {
|
|
24
11
|
humans?: string[];
|
|
25
12
|
agentSpoke?: true;
|
|
@@ -1,45 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SHARED: who the agent has HEARD in a group thread — the sole input to the participant model's summon
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* directions, and why recording is never gated on configuration). Repeating it here would mean two
|
|
6
|
-
* copies to keep true; what follows is only what a caller of this module must not get wrong.
|
|
7
|
-
*
|
|
8
|
-
* - **The key MUST be the string the session uses.** A record here is a claim about that session's
|
|
9
|
-
* memory, so the two cannot be keyed independently — including the channel brand
|
|
10
|
-
* (`feishu:<chat>:<thread>`, `slack:<team>:<channel>:<thread_ts>`). This file is already per-channel
|
|
11
|
-
* and would not need the prefix, but SESSION ids share one namespace across every channel in a
|
|
12
|
-
* deployment, so dropping it would key participation to a session that is not the one that answered.
|
|
13
|
-
* - **Write both halves under one condition**, and gate that condition on STRUCTURAL facts only (is
|
|
14
|
-
* this a group? a thread? a human speaking?) — never on configuration, which changes while records
|
|
15
|
-
* outlive the change.
|
|
16
|
-
* - **Observations only accumulate.** Nothing here sheds: no platform signals that someone stopped
|
|
17
|
-
* taking part, and the error directions are not symmetric — over-counting humans makes the agent ask
|
|
18
|
-
* to be named, under-counting makes it speak into a crowd.
|
|
19
|
-
* - Keyed by `thread_id`, never a reply-chain root: Feishu's `root_id` moves with the chain, so it
|
|
20
|
-
* cannot identify a side conversation at all.
|
|
2
|
+
* SHARED: who the agent has HEARD in a group thread — the sole input to the participant model's summon rule
|
|
3
|
+
* (docs/design/participant-model.md §3). The key MUST be the string the SESSION uses: a record here is a claim about
|
|
4
|
+
* that session's memory, so the two cannot be keyed independently.
|
|
21
5
|
*/
|
|
22
6
|
import { log } from "../../log.js";
|
|
23
7
|
import { loadStateFile, saveStateFile } from "./state.js";
|
|
24
|
-
/** Cap on remembered threads. Losing one costs a mention to re-enter that thread, so an unbounded file
|
|
25
|
-
* buys little — and a merge that carries new information rewrites the whole map synchronously, so the
|
|
26
|
-
* map's size is the cost of every such write. What keeps that bounded is that only NEW information
|
|
27
|
-
* writes at all (a repeat speaker, or any message once MAX_HUMANS is reached, returns before
|
|
28
|
-
* persisting), and that the map stays small enough for state.ts's premise to hold — these writes land
|
|
29
|
-
* synchronously on the acceptance path, so the whole file is the cost of each one. A thousand records
|
|
30
|
-
* is tens of KB, and the records that matter (threads the agent takes part in) are far fewer than
|
|
31
|
-
* that; the dominant traffic is bystanders, which is what the eviction policy below is aimed at.
|
|
32
|
-
*
|
|
33
|
-
* Eviction prefers BYSTANDER threads — ones the agent has only listened to. They are written on the
|
|
34
|
-
* same path and vastly outnumber the rest (every thread in every visible channel), yet losing one
|
|
35
|
-
* costs nothing: the summon rule refuses a thread the agent has not spoken in anyway, so the record
|
|
36
|
-
* would have to be rebuilt by the mention that admits it. Evicting purely by age would let this
|
|
37
|
-
* traffic push out the threads the agent is actively serving, silently reverting them to
|
|
38
|
-
* mention-only. This is also what makes it safe to record threads no rule currently reads (those
|
|
39
|
-
* behind a custom route, or under a posture whose summon rule is off — see the header). */
|
|
40
8
|
const MAX_THREADS = 1000;
|
|
41
|
-
/** Cap on remembered humans per thread.
|
|
42
|
-
* already the whole answer and anything beyond it is weight nothing reads. */
|
|
9
|
+
/** Cap on remembered humans per thread. */
|
|
43
10
|
const MAX_HUMANS = 2;
|
|
44
11
|
function isStoredParticipation(value) {
|
|
45
12
|
const record = value;
|
|
@@ -87,26 +54,13 @@ export function createThreadParticipants(path, label) {
|
|
|
87
54
|
const unchanged = previous !== undefined &&
|
|
88
55
|
previous.agentSpoke === next.agentSpoke &&
|
|
89
56
|
previous.humans.length === next.humans.length;
|
|
90
|
-
// Re-insert so insertion order is "least recently TOUCHED first" — including when nothing
|
|
91
|
-
// changed. A thread in its steady state (the agent answers, the same person keeps talking) stops
|
|
92
|
-
// carrying new information and would otherwise never refresh its position again, leaving the
|
|
93
|
-
// thread being served right now at the head of the eviction order.
|
|
94
|
-
//
|
|
95
|
-
// PROCESS-LOCAL: a touch alone never writes, and such a thread has nothing left to write (it has
|
|
96
|
-
// reached MAX_HUMANS), so the refreshed order survives only until restart. That is the right
|
|
97
|
-
// trade for a cache — persisting recency would mean a whole-map write per message — and the
|
|
98
|
-
// consequence is bounded: after a restart, eviction order among participant threads is the order
|
|
99
|
-
// they last carried new information.
|
|
57
|
+
// Re-insert so insertion order is "least recently TOUCHED first" — including when nothing changed.
|
|
100
58
|
records.delete(key);
|
|
101
59
|
records.set(key, next);
|
|
102
60
|
if (unchanged)
|
|
103
61
|
return;
|
|
104
62
|
while (records.size > MAX_THREADS) {
|
|
105
|
-
// Oldest bystander first; only when every record is a thread the agent takes part in does age
|
|
106
|
-
// alone decide. NEVER the key just merged: it is the most recently touched record, so evicting
|
|
107
|
-
// it contradicts the recency policy re-established above — and worse, a channel that records a
|
|
108
|
-
// thread's humans and its own participation in two steps would then write the second half onto
|
|
109
|
-
// an empty record, producing "answered here, heard nobody" and a permanent bare-message admit.
|
|
63
|
+
// Oldest bystander first; only when every record is a thread the agent takes part in does age alone decide.
|
|
110
64
|
let evict;
|
|
111
65
|
for (const [candidate, record] of records) {
|
|
112
66
|
if (candidate === key)
|
|
@@ -125,9 +79,8 @@ export function createThreadParticipants(path, label) {
|
|
|
125
79
|
saveStateFile(path, Object.fromEntries(records));
|
|
126
80
|
}
|
|
127
81
|
catch (error) {
|
|
128
|
-
// Cache only: memory stays correct for this process, and the whole map is rewritten on the
|
|
129
|
-
//
|
|
130
|
-
// whose record is lost simply needs a mention to re-enter.
|
|
82
|
+
// Cache only: memory stays correct for this process, and the whole map is rewritten on the next successful
|
|
83
|
+
// merge.
|
|
131
84
|
log.warn(`${label} could not persist thread participation ${path}: ${String(error)}`);
|
|
132
85
|
}
|
|
133
86
|
},
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one thing a chat transport needs from its CALLER, and the one place that decision is written.
|
|
3
|
+
*
|
|
4
|
+
* How a platform SIGNALS a rate limit and how long it asks us to wait are real differences and stay
|
|
5
|
+
* in each `*-api.ts` (Telegram's `parameters.retry_after`, Slack's `retry-after` header, Feishu's
|
|
6
|
+
* `code`). WHETHER A WRITE IS WORTH WAITING FOR is not a platform difference at all: it is a fact
|
|
7
|
+
* about the write, and only the caller has it.
|
|
8
|
+
*
|
|
9
|
+
* A live-preview FRAME is a full snapshot — the next frame redraws it and the terminal write
|
|
10
|
+
* supersedes it — so absorbing a rate-limit backoff for one parks the answer, and every turn queued
|
|
11
|
+
* behind it, to deliver a view nobody needs (a single 429 on a preview edit can cost ~90 s on Telegram and
|
|
12
|
+
* Slack). Such a write passes
|
|
13
|
+
* {@link DROPPABLE_FRAME}: the frame is lost, the answer is not.
|
|
14
|
+
*
|
|
15
|
+
* NOT every write. A write whose content exists only in that call keeps the default budget —
|
|
16
|
+
* Telegram's placeholder `sendMessage` (every later frame needs its id), Slack's ordered
|
|
17
|
+
* `chat.appendStream` (each append carries its own content), any terminal/settle write.
|
|
18
|
+
*/
|
|
19
|
+
/** Per-call transport options, for the distinction a transport cannot make for itself. Readonly:
|
|
20
|
+
* {@link DROPPABLE_FRAME} is one process-wide instance handed to three different transports. */
|
|
21
|
+
export interface CallOptions {
|
|
22
|
+
/** Rate-limit attempts this call absorbs. Omitted = the transport's default budget. */
|
|
23
|
+
readonly retries?: number;
|
|
24
|
+
}
|
|
25
|
+
/** A write the next one supersedes: never wait out a rate limit for it. */
|
|
26
|
+
export declare const DROPPABLE_FRAME: CallOptions;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one thing a chat transport needs from its CALLER, and the one place that decision is written.
|
|
3
|
+
*
|
|
4
|
+
* How a platform SIGNALS a rate limit and how long it asks us to wait are real differences and stay
|
|
5
|
+
* in each `*-api.ts` (Telegram's `parameters.retry_after`, Slack's `retry-after` header, Feishu's
|
|
6
|
+
* `code`). WHETHER A WRITE IS WORTH WAITING FOR is not a platform difference at all: it is a fact
|
|
7
|
+
* about the write, and only the caller has it.
|
|
8
|
+
*
|
|
9
|
+
* A live-preview FRAME is a full snapshot — the next frame redraws it and the terminal write
|
|
10
|
+
* supersedes it — so absorbing a rate-limit backoff for one parks the answer, and every turn queued
|
|
11
|
+
* behind it, to deliver a view nobody needs (a single 429 on a preview edit can cost ~90 s on Telegram and
|
|
12
|
+
* Slack). Such a write passes
|
|
13
|
+
* {@link DROPPABLE_FRAME}: the frame is lost, the answer is not.
|
|
14
|
+
*
|
|
15
|
+
* NOT every write. A write whose content exists only in that call keeps the default budget —
|
|
16
|
+
* Telegram's placeholder `sendMessage` (every later frame needs its id), Slack's ordered
|
|
17
|
+
* `chat.appendStream` (each append carries its own content), any terminal/settle write.
|
|
18
|
+
*/
|
|
19
|
+
/** A write the next one supersedes: never wait out a rate limit for it. */
|
|
20
|
+
export const DROPPABLE_FRAME = { retries: 0 };
|