@fastagent-sh/fastagent 0.21.0 → 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 -43
- 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,12 +1,17 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Clock from "effect/Clock";
|
|
3
|
+
import * as Stream from "effect/Stream";
|
|
4
|
+
import { previewPump, renderReply, serialWriter } from "../kit/delivery.js";
|
|
5
|
+
import { PortFailure, portJoin } from "../../effect-port.js";
|
|
1
6
|
import { log } from "../../log.js";
|
|
2
7
|
import { RETRY_NOTICE, THINKING_PLACEHOLDER, applyTurnEvent, composeTurnBody, createTurnView, defaultErrorMessage, humanizeToolName, revealedAnswer, summarizeToolArgs, toolLines, } from "../kit/preview-kit.js";
|
|
3
8
|
import { chunkSlackMarkdown, chunkSlackText, isSlackNativeUnavailable, } from "./slack-api.js";
|
|
9
|
+
import { DROPPABLE_FRAME } from "../kit/transport.js";
|
|
4
10
|
export { defaultErrorMessage };
|
|
5
11
|
const CLASSIC_UPDATE_INTERVAL_MS = 3_000;
|
|
6
12
|
const NATIVE_APPEND_INTERVAL_MS = 750;
|
|
7
13
|
const WORKING_STATUS = "is working on your request…";
|
|
8
14
|
const GENERIC_FAILURE = "⚠️ The response stream stopped unexpectedly. Please try again.";
|
|
9
|
-
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
15
|
/** LLM output uses standard Markdown, never Slack's notification control syntax. Keep explicit sends in
|
|
11
16
|
* the slack-send tool, where the Agent has to choose a side-effecting delivery action deliberately.
|
|
12
17
|
* The inner class excludes BOTH `<` and `>` on purpose: bounding each run by the next delimiter keeps
|
|
@@ -53,10 +58,16 @@ function withDisclaimer(markdown, disclaimer) {
|
|
|
53
58
|
const body = markdown.trim() || "(no reply)";
|
|
54
59
|
return disclaimer === false || !disclaimer?.trim() ? body : `${body}\n\n_${disclaimer.trim()}_`;
|
|
55
60
|
}
|
|
56
|
-
async function settleClassic(api, target, previewTs, markdown,
|
|
61
|
+
async function settleClassic(api, target, previewTs, markdown,
|
|
62
|
+
/** The renderer's own edit — it spends the mutation slot the pacing depends on. */
|
|
63
|
+
update) {
|
|
64
|
+
// Best-effort: the reply lands either way, but a preview left behind sits above it, so say so.
|
|
65
|
+
const dropPreview = (ts) => api
|
|
66
|
+
.deleteMessage(target.channelId, ts)
|
|
67
|
+
.catch((error) => log.warn(`[slack] could not delete the live preview ${ts}: ${String(error)}`));
|
|
57
68
|
if (markdown.trim() === "") {
|
|
58
69
|
if (previewTs)
|
|
59
|
-
await
|
|
70
|
+
await dropPreview(previewTs);
|
|
60
71
|
return;
|
|
61
72
|
}
|
|
62
73
|
const [head, ...rest] = chunkSlackMarkdown(markdown);
|
|
@@ -64,8 +75,9 @@ async function settleClassic(api, target, previewTs, markdown, update = (ts, val
|
|
|
64
75
|
try {
|
|
65
76
|
await update(previewTs, head);
|
|
66
77
|
}
|
|
67
|
-
catch {
|
|
68
|
-
|
|
78
|
+
catch (error) {
|
|
79
|
+
log.warn(`[slack] final edit of the live preview failed (${String(error)}); sending the reply as a new message`);
|
|
80
|
+
await dropPreview(previewTs);
|
|
69
81
|
await api.sendMarkdown(target, markdown);
|
|
70
82
|
return;
|
|
71
83
|
}
|
|
@@ -77,6 +89,14 @@ async function settleClassic(api, target, previewTs, markdown, update = (ts, val
|
|
|
77
89
|
}
|
|
78
90
|
await api.sendMarkdown(target, markdown);
|
|
79
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Deliver a reply this process did not generate: a recovered answer. The stream it was written for is gone with its
|
|
94
|
+
* process, so there is none to resume — but a compatibility queue message THIS process posted while the record
|
|
95
|
+
* waited its turn is taken over rather than left above the answer.
|
|
96
|
+
*/
|
|
97
|
+
export async function deliverSlackAnswer(api, target, markdown, previewTs) {
|
|
98
|
+
await settleClassic(api, target, previewTs, sanitizeSlackMarkdown(markdown), (ts, value) => api.updateMarkdown(target.channelId, ts, value));
|
|
99
|
+
}
|
|
80
100
|
/** Settle a queue/drop/defer notice. These are authored plain strings, so the basic text API is enough. */
|
|
81
101
|
export async function settleSlackPreview(api, target, previewTs, text) {
|
|
82
102
|
if (previewTs) {
|
|
@@ -85,248 +105,251 @@ export async function settleSlackPreview(api, target, previewTs, text) {
|
|
|
85
105
|
}
|
|
86
106
|
await api.postMessage(target, text);
|
|
87
107
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
127
|
-
if (previewAttempted)
|
|
108
|
+
function streamClassicSlackReply(events, api, target, formatError, initialPreviewTs, disclaimer, label, onAnswered) {
|
|
109
|
+
return Effect.gen(function* () {
|
|
110
|
+
const clock = yield* Clock.Clock;
|
|
111
|
+
const now = () => clock.currentTimeMillisUnsafe();
|
|
112
|
+
// Event → view-state reduction is the shared machine (preview-kit); this renderer owns mrkdwn
|
|
113
|
+
// sanitizing and delivery. Reasoning stays a static "Thinking…" here: raw chain-of-thought is not
|
|
114
|
+
// customer-facing on Slack, so the reducer accumulates it but this view never reads it.
|
|
115
|
+
const turn = createTurnView();
|
|
116
|
+
let previewTs = initialPreviewTs;
|
|
117
|
+
let previewAttempted = previewTs !== undefined;
|
|
118
|
+
let lastMutationAt = previewTs ? now() : 0;
|
|
119
|
+
let lastSent = "";
|
|
120
|
+
const view = () => composeTurnBody([
|
|
121
|
+
THINKING_PLACEHOLDER,
|
|
122
|
+
toolLines(turn),
|
|
123
|
+
turn.retrying ? RETRY_NOTICE : "",
|
|
124
|
+
sanitizeSlackMarkdown(revealedAnswer(turn, CLASSIC_UPDATE_INTERVAL_MS, now())),
|
|
125
|
+
]);
|
|
126
|
+
const waitForMutationSlot = Effect.suspend(() => {
|
|
127
|
+
const remaining = lastMutationAt + CLASSIC_UPDATE_INTERVAL_MS - now();
|
|
128
|
+
return remaining > 0 ? Effect.sleep(remaining) : Effect.void;
|
|
129
|
+
});
|
|
130
|
+
const update = async (ts, markdown, opts) => {
|
|
131
|
+
try {
|
|
132
|
+
await api.updateMarkdown(target.channelId, ts, markdown, opts);
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
// An ATTEMPT spends the slot, not a success. A droppable frame fails instantly on a 429, so a
|
|
136
|
+
// success-only timestamp would leave `waitForMutationSlot` open and (with throttleMs 0) re-send
|
|
137
|
+
// the same rejected view once per token — request amplification aimed at a channel that is
|
|
138
|
+
// already rate-limiting us. The failure itself still propagates to the pump's onError.
|
|
139
|
+
lastMutationAt = now();
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const flushPreview = async () => {
|
|
143
|
+
const markdown = chunkSlackText(view())[0] ?? THINKING_PLACEHOLDER;
|
|
144
|
+
if (markdown === lastSent)
|
|
128
145
|
return;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
146
|
+
if (previewTs) {
|
|
147
|
+
await update(previewTs, markdown, DROPPABLE_FRAME);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
if (previewAttempted)
|
|
151
|
+
return;
|
|
152
|
+
previewAttempted = true;
|
|
153
|
+
previewTs = await api.postMarkdown(target, markdown);
|
|
154
|
+
lastMutationAt = now();
|
|
155
|
+
}
|
|
156
|
+
lastSent = markdown;
|
|
157
|
+
};
|
|
158
|
+
const { touch, finish } = yield* previewPump({
|
|
159
|
+
flush: flushPreview,
|
|
160
|
+
throttleMs: 0,
|
|
161
|
+
beforeFlush: Effect.suspend(() => (previewTs ? waitForMutationSlot : Effect.void)),
|
|
162
|
+
onError: (error) => log.warn(`${label} live preview failed (final reply still sends): ${String(error)}`),
|
|
163
|
+
});
|
|
164
|
+
yield* renderReply(events, {
|
|
165
|
+
label,
|
|
166
|
+
finish,
|
|
167
|
+
formatError,
|
|
168
|
+
answer: () => withDisclaimer(turn.answer, disclaimer),
|
|
169
|
+
...(onAnswered ? { onAnswered } : {}),
|
|
170
|
+
settle: (markdown) => Effect.gen(function* () {
|
|
171
|
+
if (previewTs && markdown.trim())
|
|
172
|
+
yield* waitForMutationSlot;
|
|
173
|
+
yield* portJoin(() => settleClassic(api, target, previewTs, sanitizeSlackMarkdown(markdown), update));
|
|
174
|
+
}),
|
|
175
|
+
onEvent: (event) => {
|
|
176
|
+
const changed = applyTurnEvent(turn, event, now());
|
|
177
|
+
// A hidden answer must not open a placeholder followed by a rate-limited edit on a fast turn.
|
|
178
|
+
if (changed && (event.type !== "text" || revealedAnswer(turn, CLASSIC_UPDATE_INTERVAL_MS, now()) !== ""))
|
|
179
|
+
touch();
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The native renderer owns its own terminal branches instead of `renderReply`'s: each of its three
|
|
186
|
+
* endings (completed, agent failure, abnormal) weaves the ending INTO the open stream — the notice
|
|
187
|
+
* becomes more of the answer being typed out — where `renderReply` issues one separate settle write.
|
|
188
|
+
* Sharing the loop would mean injecting all three, which is more shape than the two lines it saves.
|
|
189
|
+
*/
|
|
190
|
+
function streamNativeSlackReply(events, api, target, formatError, initialPreviewTs, threadTitle, disclaimer, label, onAnswered) {
|
|
191
|
+
return Effect.gen(function* () {
|
|
192
|
+
const clock = yield* Clock.Clock;
|
|
193
|
+
const now = () => clock.currentTimeMillisUnsafe();
|
|
194
|
+
const scope = yield* Effect.scope;
|
|
195
|
+
const run = Effect.runSyncWith(yield* Effect.context());
|
|
196
|
+
let streamTs;
|
|
197
|
+
let retryStatusShown = false;
|
|
198
|
+
// Keep status updates ordered through the final clear, independently of content appends.
|
|
199
|
+
const statuses = yield* serialWriter();
|
|
200
|
+
const setStatus = (status) => {
|
|
201
|
+
statuses.enqueue(() => api
|
|
202
|
+
.setThreadStatus(target, status)
|
|
203
|
+
.catch((error) => log.warn(`${label} could not set Slack Agent status: ${String(error)}`)));
|
|
204
|
+
};
|
|
205
|
+
yield* Effect.addFinalizer(() => Effect.gen(function* () {
|
|
206
|
+
if (target.channelId.startsWith("D"))
|
|
207
|
+
setStatus("");
|
|
208
|
+
yield* statuses.finish.pipe(Effect.orDie);
|
|
209
|
+
}));
|
|
210
|
+
let stopAttempted = false;
|
|
211
|
+
const stop = (ts, markdown) => {
|
|
212
|
+
// A rejected stop may have landed; scope cleanup must not retry it.
|
|
213
|
+
stopAttempted = true;
|
|
214
|
+
return api.stopStream(target.channelId, ts, markdown);
|
|
215
|
+
};
|
|
216
|
+
yield* Effect.addFinalizer(() => Effect.suspend(() => {
|
|
217
|
+
const ts = streamTs;
|
|
218
|
+
return ts && !stopAttempted
|
|
219
|
+
? portJoin(() => stop(ts, `\n\n${GENERIC_FAILURE}`)).pipe(Effect.catchTag("PortFailure", (error) => Effect.sync(() => log.error(`${label} failed to stop an abnormal Slack stream: ${String(error.cause)}`))))
|
|
220
|
+
: Effect.void;
|
|
221
|
+
}));
|
|
222
|
+
const toolTraces = new Map();
|
|
223
|
+
let pendingText = "";
|
|
224
|
+
let fullAnswer = "";
|
|
225
|
+
let textTimer;
|
|
226
|
+
let lastTextFlushAt = 0;
|
|
227
|
+
const operations = yield* serialWriter();
|
|
228
|
+
let renderError;
|
|
229
|
+
let finalized = false;
|
|
230
|
+
let streamHasContent = false;
|
|
231
|
+
let streamEndsWithBlankLine = false;
|
|
232
|
+
const enqueue = (work) => {
|
|
233
|
+
operations.enqueue(async () => {
|
|
234
|
+
if (renderError !== undefined)
|
|
235
|
+
return;
|
|
140
236
|
try {
|
|
141
|
-
await
|
|
237
|
+
await work();
|
|
142
238
|
}
|
|
143
239
|
catch (error) {
|
|
144
|
-
|
|
145
|
-
previewErrorLogged = true;
|
|
146
|
-
log.warn(`${label} live preview failed (final reply still sends): ${String(error)}`);
|
|
147
|
-
}
|
|
240
|
+
renderError = error;
|
|
148
241
|
}
|
|
242
|
+
});
|
|
243
|
+
};
|
|
244
|
+
const sendContent = async (markdown) => {
|
|
245
|
+
if (streamTs) {
|
|
246
|
+
await api.appendStream(target.channelId, streamTs, markdown);
|
|
149
247
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
pumping = false;
|
|
153
|
-
}
|
|
154
|
-
};
|
|
155
|
-
const touch = () => {
|
|
156
|
-
dirty = true;
|
|
157
|
-
if (!pumping)
|
|
158
|
-
pumpDone = runPump();
|
|
159
|
-
};
|
|
160
|
-
const finishPump = async () => {
|
|
161
|
-
stopped = true;
|
|
162
|
-
await pumpDone?.catch(() => { });
|
|
163
|
-
};
|
|
164
|
-
const finalize = async (markdown) => {
|
|
165
|
-
await settleClassic(api, target, previewTs, sanitizeSlackMarkdown(markdown), updateRateSafe);
|
|
166
|
-
};
|
|
167
|
-
try {
|
|
168
|
-
for await (const event of events) {
|
|
169
|
-
if (event.type === "completed") {
|
|
170
|
-
await finishPump();
|
|
171
|
-
finalized = true;
|
|
172
|
-
await finalize(withDisclaimer(turn.answer, disclaimer));
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
if (event.type === "failed") {
|
|
176
|
-
await finishPump();
|
|
177
|
-
finalized = true;
|
|
178
|
-
const notice = formatError({
|
|
179
|
-
details: event.details,
|
|
180
|
-
retryable: event.retryable,
|
|
181
|
-
...(event.code !== undefined ? { code: event.code } : {}),
|
|
182
|
-
}) ?? "";
|
|
183
|
-
await finalize(notice).catch((error) => log.error(`${label} failed to deliver the agent-failure notice: ${String(error)}`));
|
|
184
|
-
throw new Error(`agent failed: ${event.details} (retryable=${event.retryable})`);
|
|
248
|
+
else {
|
|
249
|
+
streamTs = await api.startStream(target, markdown);
|
|
185
250
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
// post instead of placeholder → 3s-rate-limited edit. Thinking/tool activity still paints.
|
|
190
|
-
if (changed && (event.type !== "text" || revealedAnswer(turn, CLASSIC_UPDATE_INTERVAL_MS) !== ""))
|
|
191
|
-
touch();
|
|
192
|
-
}
|
|
193
|
-
throw new Error("stream ended without a terminal event");
|
|
194
|
-
}
|
|
195
|
-
finally {
|
|
196
|
-
await finishPump();
|
|
197
|
-
if (!finalized) {
|
|
198
|
-
const notice = formatError({ details: "the turn ended without completing", retryable: false }) ?? "";
|
|
199
|
-
await finalize(notice).catch((error) => log.error(`${label} failed to deliver the abnormal-turn notice: ${String(error)}`));
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
async function streamNativeSlackReply(events, api, target, formatError, initialPreviewTs, threadTitle, disclaimer, label) {
|
|
204
|
-
if (initialPreviewTs) {
|
|
205
|
-
await api
|
|
206
|
-
.deleteMessage(target.channelId, initialPreviewTs)
|
|
207
|
-
.catch((error) => log.warn(`${label} could not remove the compatibility queue notice before native streaming: ${String(error)}`));
|
|
208
|
-
}
|
|
209
|
-
if (target.channelId.startsWith("D")) {
|
|
210
|
-
await Promise.all([
|
|
211
|
-
api
|
|
212
|
-
.setThreadStatus(target, WORKING_STATUS)
|
|
213
|
-
.catch((error) => log.warn(`${label} could not set Slack Agent status: ${String(error)}`)),
|
|
214
|
-
threadTitle
|
|
215
|
-
? api
|
|
216
|
-
.setThreadTitle(target, threadTitle)
|
|
217
|
-
.catch((error) => log.warn(`${label} could not set Slack Agent thread title: ${String(error)}`))
|
|
218
|
-
: Promise.resolve(),
|
|
219
|
-
]);
|
|
220
|
-
}
|
|
221
|
-
let streamTs;
|
|
222
|
-
let retryStatusShown = false;
|
|
223
|
-
// DM Agent-status writes are fire-and-forget for the render loop, but must reach Slack in order:
|
|
224
|
-
// an out-of-order pair would leave a stale "retrying" line after progress (or after the final
|
|
225
|
-
// clear). One promise chain serializes them; each link swallows its own delivery error.
|
|
226
|
-
let statusChain = Promise.resolve();
|
|
227
|
-
const setStatus = (status) => {
|
|
228
|
-
statusChain = statusChain.then(() => api
|
|
229
|
-
.setThreadStatus(target, status)
|
|
230
|
-
.catch((error) => log.warn(`${label} could not set Slack Agent status: ${String(error)}`)));
|
|
231
|
-
};
|
|
232
|
-
const toolTraces = new Map();
|
|
233
|
-
let pendingText = "";
|
|
234
|
-
let fullAnswer = "";
|
|
235
|
-
let textTimer;
|
|
236
|
-
let lastTextFlushAt = 0;
|
|
237
|
-
let operation = Promise.resolve();
|
|
238
|
-
let renderError;
|
|
239
|
-
let finalized = false;
|
|
240
|
-
let streamHasContent = false;
|
|
241
|
-
let streamEndsWithBlankLine = false;
|
|
242
|
-
const enqueue = (work) => {
|
|
243
|
-
operation = operation.then(async () => {
|
|
244
|
-
if (renderError !== undefined)
|
|
251
|
+
};
|
|
252
|
+
const queueMarkdown = (markdown) => {
|
|
253
|
+
if (!markdown)
|
|
245
254
|
return;
|
|
246
|
-
|
|
247
|
-
|
|
255
|
+
streamHasContent = true;
|
|
256
|
+
streamEndsWithBlankLine = markdown.endsWith("\n\n");
|
|
257
|
+
enqueue(() => sendContent(markdown));
|
|
258
|
+
};
|
|
259
|
+
const flushText = (final = false) => {
|
|
260
|
+
if (textTimer) {
|
|
261
|
+
textTimer.interruptUnsafe();
|
|
262
|
+
textTimer = undefined;
|
|
248
263
|
}
|
|
249
|
-
|
|
250
|
-
|
|
264
|
+
let value = pendingText;
|
|
265
|
+
pendingText = "";
|
|
266
|
+
if (!final) {
|
|
267
|
+
// Hold a possible Slack control token split across Agent deltas/flush windows until its closing
|
|
268
|
+
// `>` arrives. This prevents `<` + `!channel>` from bypassing the sanitizer.
|
|
269
|
+
const open = value.lastIndexOf("<");
|
|
270
|
+
if (open >= 0 && !value.slice(open).includes(">") && value.length - open <= 256) {
|
|
271
|
+
pendingText = value.slice(open);
|
|
272
|
+
value = value.slice(0, open);
|
|
273
|
+
}
|
|
251
274
|
}
|
|
275
|
+
if (!value)
|
|
276
|
+
return;
|
|
277
|
+
lastTextFlushAt = now();
|
|
278
|
+
for (const chunk of chunkSlackText(sanitizeSlackMarkdown(value)))
|
|
279
|
+
queueMarkdown(chunk);
|
|
280
|
+
};
|
|
281
|
+
const scheduleText = () => {
|
|
282
|
+
// A zero-delay clock can finish before its handle is published.
|
|
283
|
+
if (textTimer && !textTimer.pollUnsafe())
|
|
284
|
+
return;
|
|
285
|
+
const delay = Math.max(0, lastTextFlushAt + NATIVE_APPEND_INTERVAL_MS - now());
|
|
286
|
+
textTimer = run(Effect.forkIn(Effect.sleep(delay).pipe(Effect.andThen(Effect.sync(() => {
|
|
287
|
+
textTimer = undefined;
|
|
288
|
+
flushText();
|
|
289
|
+
}))), scope));
|
|
290
|
+
};
|
|
291
|
+
// ponytail: a trace appended while the answer has an unclosed ``` fence lands inside it, and the
|
|
292
|
+
// trace's own backticks can close it early. Tracking fence parity across chunk boundaries (the job
|
|
293
|
+
// chunkSlackMarkdown does for the classic renderer) is the fix if a model is ever seen calling a
|
|
294
|
+
// tool mid-fence; the trace's blank-line framing keeps every other case well-formed.
|
|
295
|
+
const sendToolTrace = (line) => {
|
|
296
|
+
flushText();
|
|
297
|
+
const separator = streamHasContent && !streamEndsWithBlankLine ? "\n\n" : "";
|
|
298
|
+
queueMarkdown(`${separator}${line}\n\n`);
|
|
299
|
+
};
|
|
300
|
+
const settleNative = (terminalMarkdown) => Effect.gen(function* () {
|
|
301
|
+
flushText(true);
|
|
302
|
+
yield* operations.finish;
|
|
303
|
+
yield* portJoin(async () => {
|
|
304
|
+
const safeTerminal = sanitizeSlackMarkdown(terminalMarkdown);
|
|
305
|
+
if (renderError !== undefined) {
|
|
306
|
+
if (!streamTs && isSlackNativeUnavailable(renderError)) {
|
|
307
|
+
log.warn(`${label} native Slack stream was unavailable; delivering one compatibility Markdown reply`);
|
|
308
|
+
await api.sendMarkdown(target, safeTerminal);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (streamTs) {
|
|
312
|
+
await stop(streamTs, `\n\n${GENERIC_FAILURE}`).catch(() => { });
|
|
313
|
+
}
|
|
314
|
+
throw renderError;
|
|
315
|
+
}
|
|
316
|
+
if (!streamTs)
|
|
317
|
+
streamTs = await api.startStream(target, safeTerminal);
|
|
318
|
+
// What this settle had to say is on screen now — appended, or carried by the startStream above. `stop`
|
|
319
|
+
// only closes the stream, so failing it must not report the turn as undelivered: that would re-send the
|
|
320
|
+
// whole answer on the next start. The open stream is the visible cost, and the log is where it is
|
|
321
|
+
// diagnosed. (Shared by all three endings, so the wording claims a terminal write, not an answer.)
|
|
322
|
+
await stop(streamTs).catch((error) => log.warn(`${label} could not close the Slack stream after its terminal write: ${String(error)}`));
|
|
323
|
+
});
|
|
252
324
|
});
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
streamTs = await api.startStream(target, markdown);
|
|
260
|
-
}
|
|
261
|
-
};
|
|
262
|
-
const queueMarkdown = (markdown) => {
|
|
263
|
-
if (!markdown)
|
|
264
|
-
return;
|
|
265
|
-
streamHasContent = true;
|
|
266
|
-
streamEndsWithBlankLine = markdown.endsWith("\n\n");
|
|
267
|
-
enqueue(() => sendContent(markdown));
|
|
268
|
-
};
|
|
269
|
-
const flushText = (final = false) => {
|
|
270
|
-
if (textTimer) {
|
|
271
|
-
clearTimeout(textTimer);
|
|
272
|
-
textTimer = undefined;
|
|
273
|
-
}
|
|
274
|
-
let value = pendingText;
|
|
275
|
-
pendingText = "";
|
|
276
|
-
if (!final) {
|
|
277
|
-
// Hold a possible Slack control token split across Agent deltas/flush windows until its closing
|
|
278
|
-
// `>` arrives. This prevents `<` + `!channel>` from bypassing the sanitizer.
|
|
279
|
-
const open = value.lastIndexOf("<");
|
|
280
|
-
if (open >= 0 && !value.slice(open).includes(">") && value.length - open <= 256) {
|
|
281
|
-
pendingText = value.slice(open);
|
|
282
|
-
value = value.slice(0, open);
|
|
325
|
+
yield* Effect.addFinalizer(() => Effect.gen(function* () {
|
|
326
|
+
if (textTimer)
|
|
327
|
+
textTimer.interruptUnsafe();
|
|
328
|
+
if (!finalized) {
|
|
329
|
+
pendingText += `${fullAnswer.trim() ? "\n\n" : ""}${GENERIC_FAILURE}`;
|
|
330
|
+
yield* settleNative(fullAnswer.trim() || GENERIC_FAILURE).pipe(Effect.catchTag("PortFailure", (error) => Effect.sync(() => log.error(`${label} failed to stop an abnormal Slack stream: ${String(error.cause)}`))));
|
|
283
331
|
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
};
|
|
291
|
-
const scheduleText = () => {
|
|
292
|
-
if (textTimer)
|
|
293
|
-
return;
|
|
294
|
-
const delay = Math.max(0, lastTextFlushAt + NATIVE_APPEND_INTERVAL_MS - Date.now());
|
|
295
|
-
textTimer = setTimeout(() => {
|
|
296
|
-
textTimer = undefined;
|
|
297
|
-
flushText();
|
|
298
|
-
}, delay);
|
|
299
|
-
};
|
|
300
|
-
// ponytail: a trace appended while the answer has an unclosed ``` fence lands inside it, and the
|
|
301
|
-
// trace's own backticks can close it early. Tracking fence parity across chunk boundaries (the job
|
|
302
|
-
// chunkSlackMarkdown does for the classic renderer) is the fix if a model is ever seen calling a
|
|
303
|
-
// tool mid-fence; the trace's blank-line framing keeps every other case well-formed.
|
|
304
|
-
const sendToolTrace = (line) => {
|
|
305
|
-
flushText();
|
|
306
|
-
const separator = streamHasContent && !streamEndsWithBlankLine ? "\n\n" : "";
|
|
307
|
-
queueMarkdown(`${separator}${line}\n\n`);
|
|
308
|
-
};
|
|
309
|
-
const settleNative = async (terminalMarkdown) => {
|
|
310
|
-
flushText(true);
|
|
311
|
-
await operation;
|
|
312
|
-
const safeTerminal = sanitizeSlackMarkdown(terminalMarkdown);
|
|
313
|
-
if (renderError !== undefined) {
|
|
314
|
-
if (!streamTs && isSlackNativeUnavailable(renderError)) {
|
|
315
|
-
log.warn(`${label} native Slack stream was unavailable; delivering one compatibility Markdown reply`);
|
|
316
|
-
await api.sendMarkdown(target, safeTerminal);
|
|
317
|
-
return;
|
|
332
|
+
}));
|
|
333
|
+
yield* portJoin(async () => {
|
|
334
|
+
if (initialPreviewTs) {
|
|
335
|
+
await api
|
|
336
|
+
.deleteMessage(target.channelId, initialPreviewTs)
|
|
337
|
+
.catch((error) => log.warn(`${label} could not remove the compatibility queue notice before native streaming: ${String(error)}`));
|
|
318
338
|
}
|
|
319
|
-
if (
|
|
320
|
-
await
|
|
339
|
+
if (target.channelId.startsWith("D")) {
|
|
340
|
+
await Promise.all([
|
|
341
|
+
api
|
|
342
|
+
.setThreadStatus(target, WORKING_STATUS)
|
|
343
|
+
.catch((error) => log.warn(`${label} could not set Slack Agent status: ${String(error)}`)),
|
|
344
|
+
threadTitle
|
|
345
|
+
? api
|
|
346
|
+
.setThreadTitle(target, threadTitle)
|
|
347
|
+
.catch((error) => log.warn(`${label} could not set Slack Agent thread title: ${String(error)}`))
|
|
348
|
+
: Promise.resolve(),
|
|
349
|
+
]);
|
|
321
350
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (!streamTs)
|
|
325
|
-
streamTs = await api.startStream(target, safeTerminal);
|
|
326
|
-
await api.stopStream(target.channelId, streamTs);
|
|
327
|
-
};
|
|
328
|
-
try {
|
|
329
|
-
for await (const event of events) {
|
|
351
|
+
});
|
|
352
|
+
yield* Stream.runForEachWhile(events, (event) => Effect.gen(function* () {
|
|
330
353
|
if (event.type !== "retrying" && retryStatusShown) {
|
|
331
354
|
// Progress after a retry notice: restore the normal working status so the stale line doesn't
|
|
332
355
|
// contradict a visibly streaming answer.
|
|
@@ -366,46 +389,46 @@ async function streamNativeSlackReply(events, api, target, formatError, initialP
|
|
|
366
389
|
const footer = finalAnswer.slice(fullAnswer.trim().length);
|
|
367
390
|
if (footer)
|
|
368
391
|
pendingText += footer;
|
|
369
|
-
|
|
370
|
-
|
|
392
|
+
// Before the terminal write, as in `renderReply`: what the stream has already appended is not delivery —
|
|
393
|
+
// a stream left open by a dead process shows an unfinished answer.
|
|
394
|
+
yield* Effect.try({ try: () => onAnswered?.(finalAnswer), catch: (cause) => new PortFailure(cause) });
|
|
395
|
+
yield* settleNative(finalAnswer);
|
|
396
|
+
return false;
|
|
371
397
|
}
|
|
372
398
|
else if (event.type === "failed") {
|
|
373
399
|
finalized = true;
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
400
|
+
yield* Effect.gen(function* () {
|
|
401
|
+
const notice = yield* Effect.try({
|
|
402
|
+
try: () => formatError({
|
|
403
|
+
details: event.details,
|
|
404
|
+
retryable: event.retryable,
|
|
405
|
+
...(event.code !== undefined ? { code: event.code } : {}),
|
|
406
|
+
}) ?? "",
|
|
407
|
+
catch: (cause) => new PortFailure(cause),
|
|
408
|
+
});
|
|
409
|
+
if (notice) {
|
|
410
|
+
pendingText += `${fullAnswer.trim() ? "\n\n" : ""}${notice}`;
|
|
411
|
+
fullAnswer += `${fullAnswer.trim() ? "\n\n" : ""}${notice}`;
|
|
412
|
+
}
|
|
413
|
+
yield* settleNative(fullAnswer.trim() || GENERIC_FAILURE);
|
|
414
|
+
}).pipe(Effect.catchTag("PortFailure", (error) => Effect.sync(() => log.error(`${label} failed to deliver the agent-failure stream: ${String(error.cause)}`))));
|
|
415
|
+
return yield* Effect.fail(new PortFailure(new Error(`agent failed: ${event.details} (retryable=${event.retryable})`)));
|
|
385
416
|
}
|
|
417
|
+
return true;
|
|
418
|
+
}).pipe(Effect.uninterruptible));
|
|
419
|
+
if (!finalized)
|
|
420
|
+
yield* Effect.fail(new PortFailure(new Error("stream ended without a terminal event")));
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
export function slackReply(events, api, target, formatError, options = {}) {
|
|
424
|
+
return Effect.suspend(() => {
|
|
425
|
+
const { rendering = "native", initialPreviewTs, threadTitle, disclaimer, label = "[slack]", onAnswered } = options;
|
|
426
|
+
if (rendering === "native" && target.threadTs) {
|
|
427
|
+
return streamNativeSlackReply(events, api, target, formatError, initialPreviewTs, threadTitle, disclaimer, label, onAnswered);
|
|
386
428
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
finally {
|
|
390
|
-
if (textTimer)
|
|
391
|
-
clearTimeout(textTimer);
|
|
392
|
-
if (!finalized) {
|
|
393
|
-
pendingText += `${fullAnswer.trim() ? "\n\n" : ""}${GENERIC_FAILURE}`;
|
|
394
|
-
await settleNative(fullAnswer.trim() || GENERIC_FAILURE).catch((error) => log.error(`${label} failed to stop an abnormal Slack stream: ${String(error)}`));
|
|
395
|
-
}
|
|
396
|
-
if (target.channelId.startsWith("D")) {
|
|
397
|
-
setStatus("");
|
|
398
|
-
await statusChain;
|
|
429
|
+
if (rendering === "native") {
|
|
430
|
+
log.info(`${label} native streaming needs a thread target — using the classic renderer for this turn`);
|
|
399
431
|
}
|
|
400
|
-
|
|
401
|
-
}
|
|
402
|
-
export async function streamSlackReply(events, api, target, formatError, options = {}) {
|
|
403
|
-
const { rendering = "native", initialPreviewTs, threadTitle, disclaimer, label = "[slack]" } = options;
|
|
404
|
-
if (rendering === "native" && target.threadTs) {
|
|
405
|
-
return streamNativeSlackReply(events, api, target, formatError, initialPreviewTs, threadTitle, disclaimer, label);
|
|
406
|
-
}
|
|
407
|
-
if (rendering === "native") {
|
|
408
|
-
log.info(`${label} native streaming needs a thread target — using the classic renderer for this turn`);
|
|
409
|
-
}
|
|
410
|
-
return streamClassicSlackReply(events, api, target, formatError, initialPreviewTs, disclaimer, label);
|
|
432
|
+
return streamClassicSlackReply(events, api, target, formatError, initialPreviewTs, disclaimer, label, onAnswered);
|
|
433
|
+
});
|
|
411
434
|
}
|
|
@@ -14,11 +14,19 @@ export interface SlackReactionSession {
|
|
|
14
14
|
complete(): Promise<void>;
|
|
15
15
|
remove(): Promise<void>;
|
|
16
16
|
}
|
|
17
|
-
/**
|
|
18
|
-
export
|
|
17
|
+
/** What identifies the acked message: the asker's own message, and how to reach it. */
|
|
18
|
+
export interface SlackReactionTarget {
|
|
19
19
|
api: Pick<SlackApi, "addReaction" | "removeReaction">;
|
|
20
20
|
channelId: string;
|
|
21
21
|
ts: string;
|
|
22
22
|
emojis: SlackReactionEmojis;
|
|
23
23
|
label: string;
|
|
24
|
-
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Finish an ack this process did not start: 👀 on the asker's message was added by the run that produced the answer,
|
|
27
|
+
* and a re-delivery is the only thing left that can turn it into ✅. Without it the ack says "still working" under a
|
|
28
|
+
* delivered answer, forever.
|
|
29
|
+
*/
|
|
30
|
+
export declare function completeSlackReaction(target: SlackReactionTarget): Promise<void>;
|
|
31
|
+
/** Add the processing emoji now; return a session to swap it for the completed emoji or remove it. */
|
|
32
|
+
export declare function startSlackReaction(args: SlackReactionTarget): Promise<SlackReactionSession>;
|