@vellumai/assistant 0.9.0 → 0.9.1-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +18 -34
- package/bun.lock +7 -8
- package/docs/activation-funnel-telemetry.md +4 -4
- package/docs/architecture/security.md +29 -28
- package/docs/stt-provider-onboarding.md +3 -5
- package/docs/workflows-testing.md +13 -44
- package/docs/workflows.md +3 -5
- package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
- package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
- package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
- package/node_modules/@vellumai/gateway-client/src/index.ts +17 -6
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
- package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
- package/openapi.yaml +135 -59
- package/package.json +2 -1
- package/scripts/sync-llm-catalog.ts +6 -15
- package/scripts/sync-web-search-catalog.ts +3 -11
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +14 -26
- package/src/__tests__/agent-loop-compaction-strip.test.ts +240 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
- package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
- package/src/__tests__/app-dir-path-guard.test.ts +0 -1
- package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
- package/src/__tests__/avatar-identity-sync.test.ts +2 -27
- package/src/__tests__/btw-routes.test.ts +6 -8
- package/src/__tests__/checker.test.ts +0 -3
- package/src/__tests__/config-loader-backfill.test.ts +103 -6
- package/src/__tests__/config-watcher.test.ts +0 -18
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
- package/src/__tests__/credential-broker.test.ts +449 -1
- package/src/__tests__/credential-execution-tools.test.ts +0 -1
- package/src/__tests__/credential-prompt-route.test.ts +3 -4
- package/src/__tests__/credential-routes.test.ts +360 -0
- package/src/__tests__/credential-security-invariants.test.ts +2 -13
- package/src/__tests__/dynamic-page-surface.test.ts +101 -1
- package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
- package/src/__tests__/gateway-only-guard.test.ts +3 -7
- package/src/__tests__/identity-routes.test.ts +0 -189
- package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
- package/src/__tests__/invite-redemption-service.test.ts +4 -4
- package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
- package/src/__tests__/llm-catalog-parity.test.ts +0 -22
- package/src/__tests__/llm-resolver.test.ts +49 -24
- package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
- package/src/__tests__/onboarding-persona-write.test.ts +1 -1
- package/src/__tests__/persona-resolver.test.ts +11 -14
- package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
- package/src/__tests__/registry.test.ts +2 -7
- package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
- package/src/__tests__/schedule-routes.test.ts +0 -30
- package/src/__tests__/schedule-tools.test.ts +2 -18
- package/src/__tests__/skill-execute-input.test.ts +46 -1
- package/src/__tests__/skill-runtime-path.test.ts +2 -3
- package/src/__tests__/subagent-tools.test.ts +116 -0
- package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
- package/src/__tests__/token-manager.test.ts +519 -0
- package/src/__tests__/tool-executor.test.ts +0 -79
- package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
- package/src/__tests__/trusted-contact-verification.test.ts +6 -6
- package/src/__tests__/voice-invite-redemption.test.ts +2 -2
- package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
- package/src/__tests__/workspace-greetings.test.ts +152 -0
- package/src/agent/loop.ts +25 -5
- package/src/api/README.md +6 -6
- package/src/api/responses/conversation-message.ts +2 -4
- package/src/api/responses/home.ts +0 -4
- package/src/approvals/guardian-request-resolvers.ts +2 -2
- package/src/calls/relay-access-wait.ts +1 -1
- package/src/calls/voice-session-bridge.ts +2 -2
- package/src/cli/commands/plugins.ts +143 -2
- package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
- package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +313 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +253 -2
- package/src/cli/lib/diff-plugin.ts +346 -0
- package/src/cli/lib/install-from-github.ts +105 -17
- package/src/cli/lib/merge-plugin-tree.ts +228 -0
- package/src/cli/lib/plugin-fingerprint.ts +14 -0
- package/src/cli/lib/upgrade-plugin.ts +270 -10
- package/src/cli/program.ts +0 -2
- package/src/config/bundled-skills/subagent/SKILL.md +4 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
- package/src/config/bundled-skills/workflows/SKILL.md +0 -1
- package/src/config/bundled-tool-registry.ts +2 -7
- package/src/config/call-site-defaults.ts +12 -2
- package/src/config/feature-flag-registry.json +1 -17
- package/src/config/inference-profile-validation.ts +26 -0
- package/src/config/loader.ts +4 -0
- package/src/config/profile-order.ts +28 -0
- package/src/config/schemas/elevenlabs.ts +0 -1
- package/src/config/schemas/platform.ts +0 -8
- package/src/config/seed-inference-profiles.ts +25 -20
- package/src/contacts/contact-store.ts +87 -96
- package/src/contacts/contacts-write.ts +5 -21
- package/src/context/compactor.ts +2 -2
- package/src/credential-execution/process-manager.ts +55 -14
- package/src/credential-execution/prompted-credential.ts +2 -3
- package/src/daemon/config-watcher.ts +0 -4
- package/src/daemon/conversation-agent-loop.ts +15 -4
- package/src/daemon/conversation-slash.ts +2 -23
- package/src/daemon/conversation-tool-setup.ts +11 -3
- package/src/daemon/conversation.ts +2 -0
- package/src/daemon/handlers/config-channels.ts +20 -16
- package/src/daemon/handlers/config-slack-channel.ts +2 -3
- package/src/daemon/lifecycle.ts +0 -7
- package/src/daemon/message-types/conversations.ts +3 -3
- package/src/daemon/message-types/sync.ts +0 -1
- package/src/daemon/orphan-reaper.test.ts +0 -19
- package/src/daemon/orphan-reaper.ts +2 -24
- package/src/daemon/server.ts +0 -10
- package/src/home/relationship-state.ts +2 -4
- package/src/memory/__tests__/memory-retrospective-job.test.ts +195 -401
- package/src/memory/bookmark-crud.ts +1 -2
- package/src/memory/db-init.ts +7 -17
- package/src/memory/embedding-backend.ts +23 -0
- package/src/memory/embedding-billing-breaker.ts +96 -0
- package/src/memory/jobs-store.ts +25 -13
- package/src/memory/jobs-worker.ts +52 -0
- package/src/memory/memory-retrospective-constants.ts +4 -4
- package/src/memory/memory-retrospective-job.ts +19 -227
- package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +62 -0
- package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +311 -0
- package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/run-migrations.ts +41 -0
- package/src/memory/migrations/validate-migration-state.ts +1 -1
- package/src/memory/schema/contacts.ts +0 -4
- package/src/messaging/providers/slack/adapter.ts +1 -1
- package/src/notifications/adapters/shared.ts +29 -0
- package/src/notifications/adapters/slack.ts +5 -32
- package/src/notifications/adapters/telegram.ts +2 -20
- package/src/notifications/broadcaster.ts +10 -1
- package/src/notifications/home-feed-side-effect.ts +4 -3
- package/src/notifications/notification-utils.ts +17 -19
- package/src/notifications/types.ts +7 -0
- package/src/oauth/AGENTS.md +5 -24
- package/src/plugin-api/constants.ts +1 -1
- package/src/plugin-api/index.ts +6 -1
- package/src/plugin-api/model-profiles.ts +33 -0
- package/src/plugin-api/types.ts +50 -2
- package/src/plugins/defaults/index.ts +25 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +54 -2
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +107 -7
- package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
- package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
- package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
- package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
- package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +1 -1
- package/src/prompts/persona-resolver.ts +2 -2
- package/src/runtime/AGENTS.md +0 -1
- package/src/runtime/actor-trust-resolver.ts +12 -44
- package/src/runtime/btw-sidechain.ts +3 -6
- package/src/runtime/channel-approval-types.ts +18 -45
- package/src/runtime/channel-invite-transports/telegram.ts +4 -4
- package/src/runtime/channel-verification-service.ts +4 -3
- package/src/runtime/invite-redemption-service.ts +3 -3
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +218 -1
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
- package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
- package/src/runtime/routes/btw-routes.ts +1 -27
- package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +2 -2
- package/src/runtime/routes/credential-routes.ts +40 -16
- package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
- package/src/runtime/routes/identity-routes.ts +1 -296
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +1 -1
- package/src/runtime/routes/plugins-routes.ts +171 -5
- package/src/runtime/routes/schedule-routes.ts +0 -22
- package/src/runtime/routes/workflow-routes.test.ts +4 -43
- package/src/runtime/routes/workflow-routes.ts +0 -28
- package/src/runtime/routes/workspace-greetings.ts +55 -0
- package/src/runtime/sync/resource-sync-events.ts +1 -11
- package/src/schedule/inference-profile.ts +2 -14
- package/src/subagent/manager.ts +6 -0
- package/src/subagent/types.ts +6 -0
- package/src/tools/AGENTS.md +3 -3
- package/src/tools/browser/browser-execution.ts +1 -1
- package/src/tools/network/web-search-error.ts +1 -1
- package/src/tools/permission-checker.ts +1 -1
- package/src/tools/schedule/create.ts +3 -9
- package/src/tools/schedule/update.ts +2 -10
- package/src/tools/side-effects.ts +2 -17
- package/src/tools/skills/execute.ts +34 -0
- package/src/tools/subagent/spawn.ts +34 -9
- package/src/tools/tool-approval-handler.ts +1 -3
- package/src/tools/tool-manifest.ts +0 -2
- package/src/tools/ui-surface/definitions.ts +39 -1
- package/src/tools/workflows/run-workflow.test.ts +8 -18
- package/src/util/platform.ts +2 -2
- package/src/workflows/capabilities.ts +2 -3
- package/src/workflows/run-manager.test.ts +0 -25
- package/src/workflows/run-manager.ts +2 -24
- package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
- package/src/__tests__/credential-security-e2e.test.ts +0 -362
- package/src/__tests__/credential-vault-unit.test.ts +0 -1528
- package/src/__tests__/credential-vault.test.ts +0 -1706
- package/src/__tests__/identity-intro-cache.test.ts +0 -315
- package/src/__tests__/secret-onetime-send.test.ts +0 -182
- package/src/cli/commands/__tests__/task.test.ts +0 -914
- package/src/cli/commands/task.ts +0 -771
- package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
- package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
- package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
- package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
- package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
- package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
- package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
- package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
- package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
- package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
- package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
- package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
- package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
- package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
- package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
- package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
- package/src/memory/preloaded-apps.ts +0 -116
- package/src/runtime/routes/identity-intro-cache.ts +0 -172
- package/src/tools/credentials/vault.ts +0 -712
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default `post-model-call` hook: when a user-facing turn is about to end with a
|
|
3
|
+
* progress surface the model showed but never closed, nudge the model — once
|
|
4
|
+
* per run — to complete or dismiss it, then re-query so it can act.
|
|
5
|
+
*
|
|
6
|
+
* Motivation: the model can call `ui_show` with a `task_progress` card (or a
|
|
7
|
+
* `work_result` in an `in_progress` state) to show live progress, but weaker
|
|
8
|
+
* models often never advance it to a terminal status or `ui_dismiss` it. The
|
|
9
|
+
* surface then renders a spinner forever, long after the work finished. Static
|
|
10
|
+
* prompt guidance is easy to ignore; a reminder injected at the moment the turn
|
|
11
|
+
* would otherwise end — with a concrete "you left this open" signal — is far
|
|
12
|
+
* more salient.
|
|
13
|
+
*
|
|
14
|
+
* The nudge is strictly best-effort and self-targeting:
|
|
15
|
+
* - It fires at most once per run (no looping if the model declines).
|
|
16
|
+
* - It is advisory — the model may leave the surface open if the work it
|
|
17
|
+
* represents is genuinely still running.
|
|
18
|
+
* - A model that already completed or dismissed its surfaces is never nudged,
|
|
19
|
+
* so capable models that close their surfaces pay nothing.
|
|
20
|
+
*
|
|
21
|
+
* Only a finalized, no-tool, main-agent reply is actionable:
|
|
22
|
+
* - A provider rejection carries no turn content to assess (a recovery hook
|
|
23
|
+
* like history-repair owns that).
|
|
24
|
+
* - A tool-bearing turn continues naturally — the loop runs the tools and the
|
|
25
|
+
* model gets another chance to close the surface — so we leave it alone.
|
|
26
|
+
* - Background call sites (wake, title-gen, memory) have no live user watching
|
|
27
|
+
* a spinner, so the nudge would only burn a model round.
|
|
28
|
+
*
|
|
29
|
+
* No subagent guard is needed: the `ui-surface` tools are gated on a connected
|
|
30
|
+
* client (see `conversation-tool-setup.ts`), and subagents have none — so a
|
|
31
|
+
* subagent can never create a surface and so can never trigger this hook.
|
|
32
|
+
*
|
|
33
|
+
* The dangling-surface signal is derived from the current response cycle (the
|
|
34
|
+
* messages after the last genuine user prompt) by correlating each progress
|
|
35
|
+
* `ui_show` with its `surface_id` result and folding in later `ui_update` /
|
|
36
|
+
* `ui_dismiss` calls. Deriving the cycle boundary from history content rather
|
|
37
|
+
* than an index means mid-run compaction (which rewrites the array in place)
|
|
38
|
+
* can't invalidate it.
|
|
39
|
+
*
|
|
40
|
+
* The one-shot bound is split across two hooks: this hook marks the
|
|
41
|
+
* conversation when it nudges, and the sibling `stop` hook clears the mark when
|
|
42
|
+
* the turn terminates, so the next run nudges afresh.
|
|
43
|
+
*
|
|
44
|
+
* Defaults register before any user plugin, so this hook runs at the front of
|
|
45
|
+
* the `post-model-call` chain — later hooks see (and may override) its decision.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
import type { PluginHookFn, PostModelCallContext } from "@vellumai/plugin-api";
|
|
49
|
+
|
|
50
|
+
import type { ContentBlock, Message } from "../../../../providers/types.js";
|
|
51
|
+
import {
|
|
52
|
+
isSurfaceCompletionNudged,
|
|
53
|
+
markSurfaceCompletionNudged,
|
|
54
|
+
} from "../nudge-state-store.js";
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Canonical nudge text. Shown to the model as provider-only context, never to
|
|
58
|
+
* the user. Kept verbatim so a plugin that wraps the default sees a stable
|
|
59
|
+
* string. Deliberately soft: the model may leave the surface open if the work
|
|
60
|
+
* is genuinely still running.
|
|
61
|
+
*/
|
|
62
|
+
export const SURFACE_COMPLETION_NUDGE_TEXT =
|
|
63
|
+
'<system_notice>You showed the user a progress surface this turn (a task_progress card or a work_result) and are about to end the turn with it still marked in_progress. If that work is finished, advance it to a terminal state now — call ui_update to set its status to "completed" (or "failed"), or ui_dismiss it — so the user is not left watching a card spin forever. Do this only if the work it represents is actually done; if it is genuinely still running, leave it. Then give your final reply.</system_notice>';
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Surface statuses that mean the progress surface has reached a terminal state
|
|
67
|
+
* and needs no completion nudge. Covers both `task_progress`
|
|
68
|
+
* (`completed`/`failed`) and `work_result` (`completed`/`partial`/`failed`),
|
|
69
|
+
* plus `cancelled` for tolerance.
|
|
70
|
+
*/
|
|
71
|
+
const TERMINAL_STATUSES = new Set([
|
|
72
|
+
"completed",
|
|
73
|
+
"failed",
|
|
74
|
+
"partial",
|
|
75
|
+
"cancelled",
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
function hasToolUse(content: ReadonlyArray<ContentBlock>): boolean {
|
|
79
|
+
return content.some((block) => block.type === "tool_use");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A user-role message carrying only tool results, not a fresh prompt. */
|
|
83
|
+
function isToolResultMessage(message: Message): boolean {
|
|
84
|
+
return (
|
|
85
|
+
message.role === "user" &&
|
|
86
|
+
message.content.length > 0 &&
|
|
87
|
+
message.content.every((block) => block.type === "tool_result")
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Messages belonging to the current response cycle: everything after the last
|
|
93
|
+
* genuine user prompt. Falls back to the whole history when none is found.
|
|
94
|
+
*/
|
|
95
|
+
function currentCycleMessages(
|
|
96
|
+
messages: ReadonlyArray<Message>,
|
|
97
|
+
): ReadonlyArray<Message> {
|
|
98
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
99
|
+
const message = messages[i];
|
|
100
|
+
if (message.role === "user" && !isToolResultMessage(message)) {
|
|
101
|
+
return messages.slice(i + 1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return messages;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
108
|
+
return value !== null && typeof value === "object"
|
|
109
|
+
? (value as Record<string, unknown>)
|
|
110
|
+
: undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Pull a surface status out of a `ui_show` / `ui_update` input, tolerating the
|
|
115
|
+
* shapes the server-side normalization accepts: nested under
|
|
116
|
+
* `data.templateData` (task_progress), under `data` (work_result), or at the
|
|
117
|
+
* top level. Returns a lowercased status, or `undefined` when none is present.
|
|
118
|
+
*/
|
|
119
|
+
function extractStatus(input: Record<string, unknown>): string | undefined {
|
|
120
|
+
const data = asRecord(input.data);
|
|
121
|
+
const templateData =
|
|
122
|
+
asRecord(data?.templateData) ?? asRecord(input.templateData);
|
|
123
|
+
const raw = templateData?.status ?? data?.status ?? input.status;
|
|
124
|
+
return typeof raw === "string" ? raw.trim().toLowerCase() : undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Whether a `ui_show` input describes a surface with a progress lifecycle: a
|
|
129
|
+
* `task_progress` card or a `work_result`. Returns the initial status when so.
|
|
130
|
+
*/
|
|
131
|
+
function progressShowInfo(input: Record<string, unknown>): {
|
|
132
|
+
isProgress: boolean;
|
|
133
|
+
status: string | undefined;
|
|
134
|
+
} {
|
|
135
|
+
const surfaceType = input.surface_type;
|
|
136
|
+
if (surfaceType === "work_result") {
|
|
137
|
+
return { isProgress: true, status: extractStatus(input) };
|
|
138
|
+
}
|
|
139
|
+
if (surfaceType === "card") {
|
|
140
|
+
const data = asRecord(input.data);
|
|
141
|
+
const template = data?.template ?? input.template;
|
|
142
|
+
if (template === "task_progress") {
|
|
143
|
+
return { isProgress: true, status: extractStatus(input) };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { isProgress: false, status: undefined };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function surfaceIdOf(input: Record<string, unknown>): string | undefined {
|
|
150
|
+
return typeof input.surface_id === "string" ? input.surface_id : undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Parse the `{ surfaceId }` JSON a successful `ui_show` returns. */
|
|
154
|
+
function parseSurfaceId(content: string): string | undefined {
|
|
155
|
+
try {
|
|
156
|
+
const parsed = JSON.parse(content) as unknown;
|
|
157
|
+
const record = asRecord(parsed);
|
|
158
|
+
return typeof record?.surfaceId === "string" ? record.surfaceId : undefined;
|
|
159
|
+
} catch {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
interface SurfaceState {
|
|
165
|
+
/** Latest known status, lowercased; `undefined` when never set explicitly. */
|
|
166
|
+
status: string | undefined;
|
|
167
|
+
dismissed: boolean;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isNonTerminal(state: SurfaceState): boolean {
|
|
171
|
+
if (state.dismissed) return false;
|
|
172
|
+
return state.status === undefined || !TERMINAL_STATUSES.has(state.status);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* True when the current response cycle left at least one progress surface
|
|
177
|
+
* (a `task_progress` card or `work_result`) open: shown but neither advanced to
|
|
178
|
+
* a terminal status nor dismissed.
|
|
179
|
+
*
|
|
180
|
+
* Each progress `ui_show` is correlated to its `surface_id` via the matching
|
|
181
|
+
* tool result (the result lands in the next message; updates and dismisses
|
|
182
|
+
* arrive in later messages, after the model has the id in hand). Later
|
|
183
|
+
* `ui_update` / `ui_dismiss` calls fold their status / dismissal onto the
|
|
184
|
+
* tracked surface.
|
|
185
|
+
*/
|
|
186
|
+
function hasDanglingProgressSurface(messages: ReadonlyArray<Message>): boolean {
|
|
187
|
+
const surfaces = new Map<string, SurfaceState>();
|
|
188
|
+
// tool_use_id -> initial status of a progress ui_show awaiting its result id.
|
|
189
|
+
const pendingShows = new Map<string, string | undefined>();
|
|
190
|
+
|
|
191
|
+
for (const message of currentCycleMessages(messages)) {
|
|
192
|
+
if (message.role === "assistant") {
|
|
193
|
+
for (const block of message.content) {
|
|
194
|
+
if (block.type !== "tool_use") continue;
|
|
195
|
+
if (block.name === "ui_show") {
|
|
196
|
+
const info = progressShowInfo(block.input);
|
|
197
|
+
if (info.isProgress) pendingShows.set(block.id, info.status);
|
|
198
|
+
} else if (block.name === "ui_update") {
|
|
199
|
+
const id = surfaceIdOf(block.input);
|
|
200
|
+
const status = extractStatus(block.input);
|
|
201
|
+
if (id && status !== undefined) {
|
|
202
|
+
const existing = surfaces.get(id);
|
|
203
|
+
if (existing) existing.status = status;
|
|
204
|
+
else surfaces.set(id, { status, dismissed: false });
|
|
205
|
+
}
|
|
206
|
+
} else if (block.name === "ui_dismiss") {
|
|
207
|
+
const id = surfaceIdOf(block.input);
|
|
208
|
+
if (id) {
|
|
209
|
+
const existing = surfaces.get(id);
|
|
210
|
+
if (existing) existing.dismissed = true;
|
|
211
|
+
else surfaces.set(id, { status: undefined, dismissed: true });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (message.role !== "user") continue;
|
|
218
|
+
for (const block of message.content) {
|
|
219
|
+
// guard:allow-tool-result-only — only the local tool executor's
|
|
220
|
+
// `tool_result` carries a `ui_show` `surfaceId` to correlate. A
|
|
221
|
+
// `web_search_tool_result` comes from a `server_tool_use`, never a
|
|
222
|
+
// `ui_show`, so it can never match a pending show and is correctly skipped.
|
|
223
|
+
if (block.type !== "tool_result") continue;
|
|
224
|
+
if (!pendingShows.has(block.tool_use_id)) continue;
|
|
225
|
+
const initialStatus = pendingShows.get(block.tool_use_id);
|
|
226
|
+
pendingShows.delete(block.tool_use_id);
|
|
227
|
+
const id = parseSurfaceId(block.content);
|
|
228
|
+
if (!id) continue;
|
|
229
|
+
const existing = surfaces.get(id);
|
|
230
|
+
// A later update/dismiss can register the id before its show result is
|
|
231
|
+
// scanned only if history was reordered; guard so we never clobber a
|
|
232
|
+
// known terminal/dismissed state with the initial status.
|
|
233
|
+
if (existing) {
|
|
234
|
+
if (existing.status === undefined && !existing.dismissed) {
|
|
235
|
+
existing.status = initialStatus;
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
surfaces.set(id, { status: initialStatus, dismissed: false });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
for (const state of surfaces.values()) {
|
|
244
|
+
if (isNonTerminal(state)) return true;
|
|
245
|
+
}
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const postModelCall: PluginHookFn<PostModelCallContext> = async (ctx) => {
|
|
250
|
+
// A provider rejection carries no turn content to assess (a recovery hook
|
|
251
|
+
// owns the rejection).
|
|
252
|
+
if (ctx.error) return;
|
|
253
|
+
// A tool-bearing turn continues mid-run — the loop runs the tools and the
|
|
254
|
+
// model gets another chance to close the surface — so leave it alone.
|
|
255
|
+
if (hasToolUse(ctx.content)) return;
|
|
256
|
+
// Only nudge the user-facing reply: background call sites have no live user
|
|
257
|
+
// watching a spinner.
|
|
258
|
+
if (ctx.callSite !== "mainAgent") return;
|
|
259
|
+
// One nudge per run; the sibling `stop` hook clears the mark on terminal stop.
|
|
260
|
+
if (isSurfaceCompletionNudged(ctx.conversationId)) return;
|
|
261
|
+
|
|
262
|
+
if (!hasDanglingProgressSurface(ctx.messages)) return;
|
|
263
|
+
|
|
264
|
+
markSurfaceCompletionNudged(ctx.conversationId);
|
|
265
|
+
ctx.messages.push({
|
|
266
|
+
role: "user",
|
|
267
|
+
content: [{ type: "text", text: SURFACE_COMPLETION_NUDGE_TEXT }],
|
|
268
|
+
});
|
|
269
|
+
ctx.decision = "continue";
|
|
270
|
+
ctx.logger.info(
|
|
271
|
+
{ plugin: "surface-completion-nudge", conversationId: ctx.conversationId },
|
|
272
|
+
"Turn ending with an open progress surface — nudging the model to complete or dismiss it",
|
|
273
|
+
);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
export default postModelCall;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default `stop` hook: clears the per-conversation surface-completion nudge
|
|
3
|
+
* bound when a turn terminates.
|
|
4
|
+
*
|
|
5
|
+
* The `post-model-call` hook (see `./post-model-call.ts`) marks the bound when
|
|
6
|
+
* it nudges the model to close a dangling progress surface. `stop` is the
|
|
7
|
+
* definitive terminal hook — it fires exactly once when the turn is truly
|
|
8
|
+
* ending, after every retry decision has been made — so clearing the bound here
|
|
9
|
+
* unconditionally guarantees the next run nudges afresh, no matter how the turn
|
|
10
|
+
* ended (a finalized reply, an abort, or a retry the loop's per-run backstop
|
|
11
|
+
* refused).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { PluginHookFn, StopContext } from "@vellumai/plugin-api";
|
|
15
|
+
|
|
16
|
+
import { clearSurfaceCompletionNudged } from "../nudge-state-store.js";
|
|
17
|
+
|
|
18
|
+
const stop: PluginHookFn<StopContext> = async (ctx) => {
|
|
19
|
+
clearSurfaceCompletionNudged(ctx.conversationId);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default stop;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-conversation surface-completion nudge state.
|
|
3
|
+
*
|
|
4
|
+
* The `post-model-call` hook nudges the model — once per run — to complete or
|
|
5
|
+
* dismiss a progress surface it left `in_progress` when the turn was about to
|
|
6
|
+
* end, asking the loop to re-query so the model can act on it. That nudge is
|
|
7
|
+
* bounded to one pass per run: if the model declines or fails to close the
|
|
8
|
+
* surface, the hook lets the turn end rather than looping on it.
|
|
9
|
+
*
|
|
10
|
+
* The two hooks split this state's lifecycle: `post-model-call` marks a
|
|
11
|
+
* conversation when it issues the nudge, and the sibling `stop` hook clears the
|
|
12
|
+
* mark when the turn terminates. A conversation therefore only holds an entry
|
|
13
|
+
* while a nudge is in flight, and the next run nudges afresh.
|
|
14
|
+
*
|
|
15
|
+
* This module is side-effect free: importing it only initializes an empty store
|
|
16
|
+
* and registers no plugin.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Conversations with a surface-completion nudge in flight for the current run. */
|
|
20
|
+
const nudgeInFlight = new Set<string>();
|
|
21
|
+
|
|
22
|
+
/** Whether the conversation already issued its one nudge this run. */
|
|
23
|
+
export function isSurfaceCompletionNudged(conversationId: string): boolean {
|
|
24
|
+
return nudgeInFlight.has(conversationId);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Record that the conversation issued its one nudge this run. */
|
|
28
|
+
export function markSurfaceCompletionNudged(conversationId: string): void {
|
|
29
|
+
nudgeInFlight.add(conversationId);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Clear the conversation's nudge mark so the next run nudges afresh. The
|
|
34
|
+
* sibling `stop` hook calls this when the turn terminates.
|
|
35
|
+
*/
|
|
36
|
+
export function clearSurfaceCompletionNudged(conversationId: string): void {
|
|
37
|
+
nudgeInFlight.delete(conversationId);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Test-only: drop every conversation's nudge state so a suite that drives the
|
|
42
|
+
* hook directly starts each case from an empty store.
|
|
43
|
+
*/
|
|
44
|
+
export function resetSurfaceCompletionNudgeStoreForTests(): void {
|
|
45
|
+
nudgeInFlight.clear();
|
|
46
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "default-surface-completion-nudge",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "First-party default plugin that nudges the model to complete or dismiss a progress surface it left in_progress when the turn ends.",
|
|
5
|
+
"private": true,
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=20.12.0"
|
|
10
|
+
},
|
|
11
|
+
"peerDependencies": {
|
|
12
|
+
"@vellumai/plugin-api": "^0.8.0"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -56,7 +56,7 @@ import type { ContentBlock, Message } from "../../../../providers/types.js";
|
|
|
56
56
|
* are fine and the model may skip it when wrapping up.
|
|
57
57
|
*/
|
|
58
58
|
export const TASK_PROGRESS_NUDGE_TEXT =
|
|
59
|
-
'<system_notice>You are several tool calls into this turn
|
|
59
|
+
'<system_notice>You are several tool calls into this turn with no progress card shown. A card is optional, not required: if the turn is wrapping up, is not really multi-step, or you cannot form clean steps, skip it and keep working — a one-line note of what you are doing is a fine substitute, and proceeding with no card is also fine. Only if a live step tracker would genuinely help the user, show it with a SINGLE self-contained ui_show call that already contains the steps: ui_show({ surface_type: "card", data: { template: "task_progress", templateData: { title: "<what you are doing>", status: "in_progress", steps: [{ label: "<step 1>", status: "in_progress" }, { label: "<step 2>", status: "pending" }] } } }). Coarse steps are fine. Do not call ui_show with an empty `data: {}` and fill it in afterward — an empty card renders as a blank box; either include the steps now or skip the card. Advance it later with ui_update under `data.templateData`. Never let the card interrupt the actual work; if one ever looks wrong, just dismiss it and move on. You will not be nudged about this again this turn.</system_notice>';
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
62
|
* Number of tool-use rounds in a turn, with no task_progress card shown, that
|
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { basename, dirname, join } from "node:path";
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
findContactByAddress,
|
|
6
6
|
findGuardianForChannel,
|
|
7
7
|
listGuardianChannels,
|
|
8
8
|
} from "../contacts/contact-store.js";
|
|
@@ -88,7 +88,7 @@ function resolveUserFilename(
|
|
|
88
88
|
}
|
|
89
89
|
} else if (trustContext.requesterExternalUserId) {
|
|
90
90
|
// Channel-routed request — look up contact by channel identity
|
|
91
|
-
const contactWithChannels =
|
|
91
|
+
const contactWithChannels = findContactByAddress(
|
|
92
92
|
trustContext.sourceChannel,
|
|
93
93
|
trustContext.requesterExternalUserId,
|
|
94
94
|
);
|
package/src/runtime/AGENTS.md
CHANGED
|
@@ -22,7 +22,6 @@ GET handlers must be safe and side-effect-free — they must not enqueue backgro
|
|
|
22
22
|
|
|
23
23
|
Accepted exceptions (stale-while-revalidate caches): a GET handler may kick off a bounded, fire-and-forget background refresh of a generated-content cache when no fresh cache exists, provided the handler itself stays read-only and returns immediately with cached/fallback copy, the refresh is single-flight (concurrent GETs share one regeneration), and a TTL bounds regeneration frequency. Current instances:
|
|
24
24
|
|
|
25
|
-
- `GET /v1/identity/intro` — refreshes the generated greeting cache; the background prompt may only depend on static identity/soul context plus caller-supplied local hour/minute.
|
|
26
25
|
- `GET /v1/home/feed` — refreshes the personalized home greeting and suggested-prompt caches via `revalidateHomeContentInBackground()`, which publishes `home_feed_updated` when fresh content lands so clients refetch. This is intentional: home content is generated on demand (when a user actually views Home), never at daemon startup or on a timer.
|
|
27
26
|
- `GET /v1/conversation-starters` — enqueues a `generate_conversation_starters` memory job when the starter set is stale, cooldown-gated and deduped against in-flight jobs.
|
|
28
27
|
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
import type { ChannelId } from "../channels/types.js";
|
|
15
15
|
import {
|
|
16
16
|
findContactByAddress,
|
|
17
|
-
findContactByChannelExternalId,
|
|
18
17
|
findGuardianForChannel,
|
|
19
18
|
} from "../contacts/contact-store.js";
|
|
20
19
|
import type { ContactChannel, ContactWithChannels } from "../contacts/types.js";
|
|
@@ -181,19 +180,14 @@ export function resolveActorTrust(
|
|
|
181
180
|
if (guardianResult) {
|
|
182
181
|
const { contact: guardianContact, channel: guardianChannel } =
|
|
183
182
|
guardianResult;
|
|
184
|
-
const canonicalGuardianId = guardianChannel.externalUserId
|
|
185
|
-
? canonicalizeInboundIdentity(
|
|
186
|
-
input.sourceChannel,
|
|
187
|
-
guardianChannel.externalUserId,
|
|
188
|
-
)
|
|
189
|
-
: null;
|
|
190
183
|
guardianBindingMatch = {
|
|
191
|
-
guardianExternalUserId:
|
|
184
|
+
guardianExternalUserId:
|
|
185
|
+
guardianChannel.externalUserId ?? guardianChannel.address ?? "",
|
|
192
186
|
guardianDeliveryChatId: guardianChannel.externalChatId,
|
|
193
187
|
};
|
|
194
188
|
guardianPrincipalId = guardianContact.principalId ?? undefined;
|
|
195
189
|
isGuardian =
|
|
196
|
-
|
|
190
|
+
guardianChannel.address.toLowerCase() === canonicalSenderId.toLowerCase();
|
|
197
191
|
}
|
|
198
192
|
|
|
199
193
|
log.debug(
|
|
@@ -206,37 +200,18 @@ export function resolveActorTrust(
|
|
|
206
200
|
);
|
|
207
201
|
|
|
208
202
|
// --- Member lookup via contacts ---
|
|
209
|
-
// Primary path: match by externalUserId (populated after channel verification
|
|
210
|
-
// completes, or for channels registered via the verification upsert path).
|
|
211
|
-
// Fallback path: match by address (covers channels registered by the inbound
|
|
212
|
-
// name-capture flow, where address is set but externalUserId remains NULL
|
|
213
|
-
// until the DTMF challenge succeeds). Mirrors the gateway's OR-based lookup
|
|
214
|
-
// in ContactStore.getContactByPhoneNumber so the runtime's unverified-caller
|
|
215
|
-
// guard fires for pre-verification channels the gateway passes through.
|
|
216
203
|
let memberRecord: ActorTrustContext["memberRecord"] = null;
|
|
217
|
-
const
|
|
204
|
+
const byAddress = findContactByAddress(
|
|
218
205
|
input.sourceChannel,
|
|
219
206
|
canonicalSenderId,
|
|
220
207
|
);
|
|
221
|
-
const
|
|
208
|
+
const byAddressChannel = byAddress?.channels.find(
|
|
222
209
|
(ch) =>
|
|
223
210
|
ch.type === input.sourceChannel &&
|
|
224
|
-
ch.
|
|
211
|
+
ch.address.toLowerCase() === canonicalSenderId.toLowerCase(),
|
|
225
212
|
);
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
memberRecord = { contact: byExternalId, channel: byExternalIdChannel };
|
|
229
|
-
} else {
|
|
230
|
-
// Address fallback: catches channels where externalUserId is not yet set.
|
|
231
|
-
const byAddress = findContactByAddress(input.sourceChannel, canonicalSenderId);
|
|
232
|
-
const byAddressChannel = byAddress?.channels.find(
|
|
233
|
-
(ch) =>
|
|
234
|
-
ch.type === input.sourceChannel &&
|
|
235
|
-
ch.address?.toLowerCase() === canonicalSenderId.toLowerCase(),
|
|
236
|
-
);
|
|
237
|
-
if (byAddress && byAddressChannel) {
|
|
238
|
-
memberRecord = { contact: byAddress, channel: byAddressChannel };
|
|
239
|
-
}
|
|
213
|
+
if (byAddress && byAddressChannel) {
|
|
214
|
+
memberRecord = { contact: byAddress, channel: byAddressChannel };
|
|
240
215
|
}
|
|
241
216
|
|
|
242
217
|
log.debug(
|
|
@@ -244,23 +219,16 @@ export function resolveActorTrust(
|
|
|
244
219
|
channel: input.sourceChannel,
|
|
245
220
|
canonicalSenderId,
|
|
246
221
|
found: !!memberRecord,
|
|
247
|
-
via: memberRecord
|
|
222
|
+
via: memberRecord ? "address" : "none",
|
|
248
223
|
},
|
|
249
224
|
"trust-resolver member lookup",
|
|
250
225
|
);
|
|
251
226
|
|
|
252
227
|
// Only use member metadata when the record's channel identity matches the
|
|
253
228
|
// current sender to avoid misidentification in group chats.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const memberMatchesSender = memberRecord?.channel.externalUserId
|
|
258
|
-
? canonicalizeInboundIdentity(
|
|
259
|
-
input.sourceChannel,
|
|
260
|
-
memberRecord.channel.externalUserId,
|
|
261
|
-
) === canonicalSenderId
|
|
262
|
-
: (memberRecord?.channel.address?.toLowerCase() ===
|
|
263
|
-
canonicalSenderId.toLowerCase());
|
|
229
|
+
const memberMatchesSender =
|
|
230
|
+
memberRecord?.channel.address.toLowerCase() ===
|
|
231
|
+
canonicalSenderId.toLowerCase();
|
|
264
232
|
|
|
265
233
|
const memberDisplayName =
|
|
266
234
|
memberMatchesSender &&
|
|
@@ -31,10 +31,9 @@ export interface RunBtwSidechainParams {
|
|
|
31
31
|
/**
|
|
32
32
|
* Unified call-site identifier. The provider layer resolves
|
|
33
33
|
* provider/model/maxTokens/effort/speed/temperature/thinking/contextWindow
|
|
34
|
-
* via `resolveCallSiteConfig(callSite, config.llm)`.
|
|
35
|
-
* `
|
|
36
|
-
*
|
|
37
|
-
* override it with their own call-site ID.
|
|
34
|
+
* via `resolveCallSiteConfig(callSite, config.llm)`. Generic BTW traffic
|
|
35
|
+
* uses `identityIntro`; callers with a narrower purpose override it with
|
|
36
|
+
* their own call-site ID.
|
|
38
37
|
*/
|
|
39
38
|
callSite?: LLMCallSite;
|
|
40
39
|
signal?: AbortSignal;
|
|
@@ -95,8 +94,6 @@ export async function runBtwSidechain(
|
|
|
95
94
|
config: {
|
|
96
95
|
max_tokens: params.maxTokens ?? 1024,
|
|
97
96
|
tool_choice: { type: "none" },
|
|
98
|
-
// Default call site is "identityIntro" — the original purpose of
|
|
99
|
-
// this side-chain runner. Callers may override per invocation.
|
|
100
97
|
callSite: params.callSite ?? ("identityIntro" as LLMCallSite),
|
|
101
98
|
},
|
|
102
99
|
onEvent: (event) => {
|
|
@@ -1,27 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Channel-agnostic approval flow types.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Wire-format types (`ApprovalUIMetadata`, `PermissionRequestDetails`,
|
|
5
|
+
* `ApprovalActionOption`) are defined as Zod schemas in
|
|
6
|
+
* `@vellumai/gateway-client/outbound-contract` and re-exported here for
|
|
7
|
+
* convenience. Daemon-internal types that do not cross a wire boundary
|
|
8
|
+
* are defined locally.
|
|
8
9
|
*/
|
|
9
10
|
|
|
11
|
+
import type { ApprovalActionOption } from "@vellumai/gateway-client";
|
|
12
|
+
|
|
10
13
|
import type { GuardianDecisionAction } from "./guardian-decision-types.js";
|
|
11
14
|
|
|
15
|
+
// Re-export shared wire types so existing daemon imports keep working.
|
|
16
|
+
export type {
|
|
17
|
+
ApprovalActionOption,
|
|
18
|
+
ApprovalUIMetadata,
|
|
19
|
+
PermissionRequestDetails,
|
|
20
|
+
} from "@vellumai/gateway-client";
|
|
21
|
+
|
|
12
22
|
// ---------------------------------------------------------------------------
|
|
13
|
-
// Approval actions
|
|
23
|
+
// Approval actions (daemon-internal)
|
|
14
24
|
// ---------------------------------------------------------------------------
|
|
15
25
|
|
|
16
26
|
/** The set of actions a user can take on an approval prompt. */
|
|
17
27
|
export type ApprovalAction = "approve_once" | "reject";
|
|
18
28
|
|
|
19
|
-
/** An action presented to the user as a tappable button or text option. */
|
|
20
|
-
export interface ApprovalActionOption {
|
|
21
|
-
id: ApprovalAction;
|
|
22
|
-
label: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
29
|
/**
|
|
26
30
|
* Map `GuardianDecisionAction[]` to `ApprovalActionOption[]` so channel
|
|
27
31
|
* prompt payloads can be derived from the unified decision action set.
|
|
@@ -32,13 +36,13 @@ export function toApprovalActionOptions(
|
|
|
32
36
|
actions: GuardianDecisionAction[],
|
|
33
37
|
): ApprovalActionOption[] {
|
|
34
38
|
return actions.map((a) => ({
|
|
35
|
-
id: a.action
|
|
39
|
+
id: a.action,
|
|
36
40
|
label: a.label,
|
|
37
41
|
}));
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
// ---------------------------------------------------------------------------
|
|
41
|
-
// Approval prompt
|
|
45
|
+
// Approval prompt (daemon-internal)
|
|
42
46
|
// ---------------------------------------------------------------------------
|
|
43
47
|
|
|
44
48
|
/** The approval prompt model sent to users via a channel. */
|
|
@@ -52,38 +56,7 @@ export interface ChannelApprovalPrompt {
|
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
// ---------------------------------------------------------------------------
|
|
55
|
-
//
|
|
56
|
-
// ---------------------------------------------------------------------------
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Tool-permission-specific details carried alongside the approval payload.
|
|
60
|
-
* Channels that support rich UI (e.g. Slack Block Kit) use these fields
|
|
61
|
-
* to render a detailed permission request card with risk indicators,
|
|
62
|
-
* tool arguments, and requester identity.
|
|
63
|
-
*/
|
|
64
|
-
export interface PermissionRequestDetails {
|
|
65
|
-
toolName: string;
|
|
66
|
-
riskLevel: string;
|
|
67
|
-
toolInput: Record<string, unknown>;
|
|
68
|
-
/** Present for guardian-escalated requests to identify who is asking. */
|
|
69
|
-
requesterIdentifier?: string;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Metadata attached to gateway callback payloads so the channel adapter
|
|
74
|
-
* can render approval UI and route the user's decision back to the
|
|
75
|
-
* correct pending interaction.
|
|
76
|
-
*/
|
|
77
|
-
export interface ApprovalUIMetadata {
|
|
78
|
-
requestId: string;
|
|
79
|
-
actions: ApprovalActionOption[];
|
|
80
|
-
plainTextFallback: string;
|
|
81
|
-
/** When present, the approval is a tool permission request with extra context. */
|
|
82
|
-
permissionDetails?: PermissionRequestDetails;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
// Decision result
|
|
59
|
+
// Decision result (daemon-internal)
|
|
87
60
|
// ---------------------------------------------------------------------------
|
|
88
61
|
|
|
89
62
|
/** How the user communicated their decision. */
|
|
@@ -36,10 +36,10 @@ import type {
|
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Ensure the Telegram bot username is resolved and cached in config.
|
|
39
|
-
* When the bot token was configured via
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
39
|
+
* When the bot token was configured via the `assistant credentials set` CLI
|
|
40
|
+
* or ingress secret redirect, the `getMe` API call that populates the config
|
|
41
|
+
* is skipped — this function fills that gap so that invite share links can be
|
|
42
|
+
* generated.
|
|
43
43
|
*/
|
|
44
44
|
export async function ensureTelegramBotUsernameResolved(): Promise<void> {
|
|
45
45
|
if (getTelegramBotUsername() && getTelegramBotId()) {
|
|
@@ -333,7 +333,8 @@ export function getGuardianBinding(
|
|
|
333
333
|
id: result.channel.id,
|
|
334
334
|
assistantId,
|
|
335
335
|
channel,
|
|
336
|
-
guardianExternalUserId:
|
|
336
|
+
guardianExternalUserId:
|
|
337
|
+
result.channel.externalUserId ?? result.channel.address ?? "",
|
|
337
338
|
guardianDeliveryChatId: result.channel.externalChatId ?? "",
|
|
338
339
|
guardianPrincipalId: result.contact.principalId ?? "",
|
|
339
340
|
status: "active" as const,
|
|
@@ -355,11 +356,11 @@ export function getGuardianBinding(
|
|
|
355
356
|
export function isGuardian(
|
|
356
357
|
assistantId: string,
|
|
357
358
|
channel: string,
|
|
358
|
-
|
|
359
|
+
address: string,
|
|
359
360
|
): boolean {
|
|
360
361
|
const result = findGuardianForChannel(channel);
|
|
361
362
|
if (result) {
|
|
362
|
-
return result.channel.
|
|
363
|
+
return result.channel.address.toLowerCase() === address.toLowerCase();
|
|
363
364
|
}
|
|
364
365
|
|
|
365
366
|
return false;
|