@vellumai/assistant 0.12.2 → 0.12.3-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/.env.example +2 -0
- package/AGENTS.md +1 -1
- package/docs/architecture/integrations.md +7 -0
- package/docs/architecture/memory.md +17 -2
- package/docs/credential-execution-service.md +1 -1
- package/docs/desktop-browser-cli.md +7 -3
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
- package/node_modules/@vellumai/environments/src/shell.test.ts +21 -0
- package/node_modules/@vellumai/environments/src/shell.ts +24 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +8 -2
- package/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
- package/openapi.yaml +54 -5
- package/package.json +1 -1
- package/scripts/smoke-desktop-browser-cli.ts +1 -0
- package/scripts/sync-llm-catalog.ts +3 -0
- package/scripts/voice-ttft-spike.ts +2 -2
- package/src/__tests__/agent-loop.test.ts +259 -0
- package/src/__tests__/agent-wake-delegation-prompt.test.ts +64 -2
- package/src/__tests__/anthropic-provider.test.ts +55 -0
- package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
- package/src/__tests__/attachments-store.test.ts +22 -3
- package/src/__tests__/channel-approval.test.ts +9 -14
- package/src/__tests__/channel-reply-delivery.test.ts +56 -0
- package/src/__tests__/chat-credential-redaction.test.ts +23 -0
- package/src/__tests__/computer-use-screenshot-attachments.test.ts +411 -0
- package/src/__tests__/computer-use-screenshot-selection.test.ts +50 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop.test.ts +257 -6
- package/src/__tests__/conversation-attachments.test.ts +106 -0
- package/src/__tests__/conversation-fork-crud.test.ts +191 -0
- package/src/__tests__/conversation-rendered-delegation-state.test.ts +276 -0
- package/src/__tests__/credential-execution-client.test.ts +70 -53
- package/src/__tests__/db-conversation-tool-surface.test.ts +221 -0
- package/src/__tests__/events-tail-route.test.ts +33 -0
- package/src/__tests__/history-repair.test.ts +53 -0
- package/src/__tests__/inference-profile-session-handler.test.ts +26 -0
- package/src/__tests__/list-messages-tool-merge.test.ts +85 -1
- package/src/__tests__/llm-catalog-parity.test.ts +27 -4
- package/src/__tests__/oauth-apps-routes.test.ts +1 -0
- package/src/__tests__/oauth-commands-routes.test.ts +114 -101
- package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
- package/src/__tests__/oauth-providers-routes.test.ts +2 -0
- package/src/__tests__/persist-media-references.test.ts +50 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
- package/src/__tests__/run-conversation-turn-persistence.test.ts +138 -1
- package/src/__tests__/schedule-routes.test.ts +20 -0
- package/src/__tests__/scheduler-result-notification.test.ts +23 -4
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/secret-routes-platform-proxy.test.ts +86 -31
- package/src/__tests__/secret-routes-scrub.test.ts +22 -27
- package/src/__tests__/secret-scanner.test.ts +20 -0
- package/src/__tests__/secure-keys-managed-failover.test.ts +8 -0
- package/src/__tests__/secure-keys.test.ts +7 -3
- package/src/__tests__/server-tool-pairing.test.ts +107 -0
- package/src/__tests__/skills.test.ts +5 -4
- package/src/__tests__/subagent-tool-gate-mode.test.ts +226 -0
- package/src/__tests__/terminal-tools.test.ts +8 -0
- package/src/__tests__/tool-result-follow-up.test.ts +99 -0
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +63 -0
- package/src/__tests__/unicode.test.ts +36 -0
- package/src/agent/loop.ts +37 -12
- package/src/agent/tool-result-follow-up.ts +70 -0
- package/src/api/attachment-provenance.test.ts +68 -0
- package/src/api/computer-use-tool.test.ts +55 -0
- package/src/api/computer-use-tool.ts +27 -0
- package/src/api/events/assistant-outbound-attachment.ts +3 -0
- package/src/api/events/desktop-activity-changed.ts +10 -0
- package/src/api/events/question-request.ts +1 -0
- package/src/api/index.ts +11 -0
- package/src/api/responses/conversation-message.ts +3 -0
- package/src/approvals/approval-primitive.ts +5 -2
- package/src/approvals/scoped-approval-grants.ts +6 -2
- package/src/browser/virtual-desktop-target.ts +1 -2
- package/src/cli/commands/__tests__/cli-test-harness.ts +21 -3
- package/src/cli/commands/__tests__/plugins.test.ts +20 -1
- package/src/cli/commands/__tests__/schedules.test.ts +14 -0
- package/src/cli/commands/bash.help.ts +4 -3
- package/src/cli/commands/browser.help.ts +5 -1
- package/src/cli/commands/credentials.help.ts +3 -3
- package/src/cli/commands/oauth/index.help.ts +10 -0
- package/src/cli/commands/oauth/providers.ts +12 -0
- package/src/cli/commands/oauth/request.test.ts +179 -2
- package/src/cli/commands/oauth/request.ts +41 -10
- package/src/cli/commands/plugins.ts +12 -5
- package/src/cli/commands/schedules.ts +2 -0
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +41 -0
- package/src/cli/lib/__tests__/local-plugin-upgrade.test.ts +133 -51
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +8 -2
- package/src/cli/lib/bundled-marketplace.json +855 -0
- package/src/cli/lib/bundled-plugin-packages.json +783 -1
- package/src/cli/lib/inspect-plugin.ts +11 -4
- package/src/cli/lib/upgrade-plugin.ts +19 -11
- package/src/cli/output.ts +18 -4
- package/src/config/__tests__/default-provider.test.ts +4 -0
- package/src/config/bundled-skills/schedule/SKILL.md +6 -6
- package/src/config/loader.ts +9 -17
- package/src/config/platform-identity.ts +8 -8
- package/src/config/profile-text-generation.test.ts +51 -0
- package/src/config/profile-text-generation.ts +51 -0
- package/src/config/schemas/__tests__/memory-v3.test.ts +12 -0
- package/src/config/schemas/llm.ts +7 -2
- package/src/config/schemas/mcp.ts +5 -1
- package/src/config/schemas/memory-v3.ts +12 -0
- package/src/credential-execution/ces-runtime.ts +2 -2
- package/src/credential-execution/executable-discovery.ts +36 -134
- package/src/credential-execution/process-manager.test.ts +20 -24
- package/src/credential-execution/process-manager.ts +24 -19
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +43 -0
- package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +36 -0
- package/src/daemon/assistant-attachments.ts +20 -12
- package/src/daemon/chat-credential-redaction.ts +16 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +74 -3
- package/src/daemon/conversation-agent-loop.ts +12 -0
- package/src/daemon/conversation-attachments.ts +93 -12
- package/src/daemon/conversation-tool-setup.ts +71 -3
- package/src/daemon/conversation-turn-finalize.ts +26 -13
- package/src/daemon/conversation.ts +72 -17
- package/src/daemon/daemon-control.ts +3 -7
- package/src/daemon/lifecycle.ts +4 -4
- package/src/daemon/mcp-reload-service.ts +1 -1
- package/src/daemon/message-types/shared.ts +1 -0
- package/src/daemon/orphan-reaper.ts +4 -3
- package/src/daemon/persist-media-references.ts +20 -3
- package/src/daemon/tool-setup-types.ts +6 -0
- package/src/daemon/wake-conversation-ops.ts +50 -15
- package/src/desktop/desktop-automation-lease.test.ts +230 -0
- package/src/desktop/desktop-automation-lease.ts +90 -6
- package/src/desktop/desktop-help.ts +15 -0
- package/src/desktop/desktop-stream-bridge.test.ts +65 -10
- package/src/desktop/desktop-stream-bridge.ts +3 -3
- package/src/desktop/desktop-wallpaper-renderer.ts +132 -0
- package/src/desktop/desktop-wallpaper-worker.ts +17 -0
- package/src/desktop/desktop-wallpaper.test.ts +48 -7
- package/src/desktop/desktop-wallpaper.ts +34 -102
- package/src/desktop/virtual-desktop-feature.ts +1 -1
- package/src/mcp/__tests__/manager-tool-caps.test.ts +111 -0
- package/src/mcp/__tests__/startup.test.ts +30 -7
- package/src/mcp/__tests__/tool-caps.test.ts +107 -0
- package/src/mcp/manager.ts +168 -101
- package/src/mcp/startup.ts +33 -13
- package/src/mcp/tool-caps.ts +176 -0
- package/src/messaging/provider-message-metadata.ts +3 -3
- package/src/notifications/__tests__/copy-composer.test.ts +70 -0
- package/src/notifications/copy-composer.ts +11 -3
- package/src/notifications/schedule-result-producer.ts +4 -6
- package/src/oauth/AGENTS.md +2 -0
- package/src/oauth/__tests__/identity-verifier.test.ts +36 -1
- package/src/oauth/identity-verifier.ts +24 -0
- package/src/oauth/oauth-store.ts +11 -1
- package/src/oauth/provider-serializer.ts +1 -0
- package/src/oauth/seed-providers.ts +12 -0
- package/src/permissions/confirmation-guardian-request.test.ts +16 -0
- package/src/permissions/confirmation-guardian-request.ts +2 -3
- package/src/permissions/question-prompter.test.ts +31 -0
- package/src/permissions/question-prompter.ts +2 -0
- package/src/persistence/attachments-store.ts +84 -44
- package/src/persistence/conversation-crud.ts +111 -20
- package/src/persistence/conversation-plugin-facade.ts +30 -0
- package/src/persistence/conversation-tool-surface.ts +135 -0
- package/src/persistence/conversation-types.test.ts +32 -0
- package/src/persistence/conversation-types.ts +26 -4
- package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +78 -0
- package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +29 -0
- package/src/persistence/migrations/379-oauth-providers-response-ok-field.test.ts +92 -0
- package/src/persistence/migrations/379-oauth-providers-response-ok-field.ts +32 -0
- package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.test.ts +90 -0
- package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.ts +34 -0
- package/src/persistence/schema/conversation-tool-surfaces.ts +32 -0
- package/src/persistence/schema/index.ts +1 -0
- package/src/persistence/schema/oauth.ts +1 -0
- package/src/persistence/steps.ts +24 -3
- package/src/plugin-api/conversation-turn.ts +31 -7
- package/src/plugin-api/index.ts +11 -1
- package/src/plugin-api/plugin-channel-turn-trust.test.ts +133 -0
- package/src/plugin-api/plugin-channel-turn-trust.ts +71 -0
- package/src/plugins/defaults/memory/AGENTS.md +39 -5
- package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +365 -0
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +43 -0
- package/src/plugins/defaults/memory/__tests__/conversation-memory-purge.test.ts +1 -0
- package/src/plugins/defaults/memory/__tests__/db-memory-attach.test.ts +2 -0
- package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +17 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +74 -0
- package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +161 -0
- package/src/plugins/defaults/memory/__tests__/relocated-memory-test-rows.ts +10 -0
- package/src/plugins/defaults/memory/buffer-file.ts +354 -0
- package/src/plugins/defaults/memory/buffer-format.ts +40 -0
- package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -2
- package/src/plugins/defaults/memory/context-search/format.ts +2 -1
- package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +2 -1
- package/src/plugins/defaults/memory/context-search/sources/workspace.ts +2 -1
- package/src/plugins/defaults/memory/conversation-memory-purge.ts +4 -0
- package/src/plugins/defaults/memory/graph/capability-seed.ts +1 -2
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -42
- package/src/plugins/defaults/memory/host-utils.ts +0 -10
- package/src/plugins/defaults/memory/injectors.ts +4 -3
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +72 -183
- package/src/plugins/defaults/memory/memory-run-evidence.ts +213 -0
- package/src/plugins/defaults/memory/src/memory-item-routes.test.ts +1 -1
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +407 -99
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +10 -0
- package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +107 -7
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +307 -86
- package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +34 -0
- package/src/plugins/defaults/memory/substrate/page-index.ts +2 -1
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +89 -49
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +1 -1
- package/src/plugins/defaults/memory/tools.ts +1 -1
- package/src/plugins/defaults/memory/v1/graph/consolidation.ts +2 -2
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +2 -1
- package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
- package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +5 -0
- package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +5 -2
- package/src/plugins/defaults/memory/v2/reranker.ts +2 -1
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +81 -1
- package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +87 -0
- package/src/plugins/defaults/memory/v3/__tests__/plugin-schema.test.ts +12 -0
- package/src/plugins/defaults/memory/v3/__tests__/pool-log-store.test.ts +183 -4
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +103 -2
- package/src/plugins/defaults/memory/v3/card.ts +2 -1
- package/src/plugins/defaults/memory/v3/injector.ts +212 -178
- package/src/plugins/defaults/memory/v3/orchestrate.ts +112 -25
- package/src/plugins/defaults/memory/v3/plugin-schema.ts +54 -4
- package/src/plugins/defaults/memory/v3/pool-log-store.ts +253 -1
- package/src/plugins/defaults/memory/v3/pool-select.test.ts +35 -2
- package/src/plugins/defaults/memory/v3/pool-select.ts +31 -19
- package/src/plugins/defaults/memory/v3/sections.ts +2 -1
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +41 -14
- package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +4 -1
- package/src/plugins/defaults/tool-result-truncate/terminal.ts +1 -46
- package/src/prompts/__tests__/parallel-tasks-section.test.ts +25 -0
- package/src/prompts/delegation-gate.ts +57 -0
- package/src/prompts/system-prompt.ts +14 -32
- package/src/providers/inference/adapter-factory.ts +6 -0
- package/src/providers/jev/client.test.ts +260 -0
- package/src/providers/jev/client.ts +518 -0
- package/src/providers/model-catalog.ts +58 -3
- package/src/providers/server-tool-pairing.ts +16 -7
- package/src/runtime/AGENTS.md +2 -2
- package/src/runtime/__tests__/agent-wake.test.ts +94 -1
- package/src/runtime/agent-wake.ts +31 -4
- package/src/runtime/guardian-action-service.ts +2 -17
- package/src/runtime/guardian-reply-router.ts +1 -8
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +89 -0
- package/src/runtime/migrations/vbundle-import-policy.ts +33 -11
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +24 -2
- package/src/runtime/routes/channel-route-shared.ts +1 -9
- package/src/runtime/routes/conversation-routes.ts +29 -4
- package/src/runtime/routes/desktop-setup-routes.test.ts +2 -2
- package/src/runtime/routes/desktop-setup-routes.ts +8 -5
- package/src/runtime/routes/guardian-approval-interception.ts +24 -0
- package/src/runtime/routes/inbound-message-handler.ts +2 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +1 -1
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -4
- package/src/runtime/routes/inference-profile-session-handler.ts +11 -1
- package/src/runtime/routes/inference-profiles-routes.ts +9 -0
- package/src/runtime/routes/oauth-commands-routes.ts +57 -54
- package/src/runtime/routes/oauth-providers.ts +4 -0
- package/src/runtime/routes/oauth-request-hints.test.ts +246 -0
- package/src/runtime/routes/oauth-request-hints.ts +149 -0
- package/src/runtime/routes/schedule-routes.ts +2 -0
- package/src/runtime/routes/secret-routes.ts +99 -119
- package/src/schedule/__tests__/worker-mcp-bootstrap.test.ts +107 -0
- package/src/schedule/__tests__/worker-mcp-readiness.test.ts +173 -0
- package/src/schedule/__tests__/worker-mcp-tools.test.ts +13 -47
- package/src/schedule/run-script.ts +2 -2
- package/src/schedule/scheduler.ts +47 -11
- package/src/schedule/tool-surface-readiness.ts +65 -0
- package/src/schedule/worker-mcp.ts +85 -0
- package/src/schedule/worker.ts +9 -31
- package/src/security/secure-keys.ts +60 -48
- package/src/tools/ask-question/ask-question-tool.test.ts +167 -5
- package/src/tools/ask-question/ask-question-tool.ts +119 -23
- package/src/tools/browser/browser-execution.ts +7 -1
- package/src/tools/host-terminal/host-shell.ts +12 -6
- package/src/tools/shared/filesystem/file-ops-service.ts +1 -31
- package/src/tools/shared/shell-output.test.ts +10 -0
- package/src/tools/shared/shell-output.ts +14 -2
- package/src/tools/skills/sandbox-runner.ts +13 -2
- package/src/tools/skills/scaffold-managed.ts +2 -1
- package/src/tools/terminal/__tests__/safe-env.test.ts +33 -4
- package/src/tools/terminal/__tests__/sanitized-bash.test.ts +0 -14
- package/src/tools/terminal/safe-env.ts +35 -24
- package/src/tools/terminal/sanitized-bash.ts +15 -2
- package/src/tools/terminal/shell-launch.test.ts +162 -0
- package/src/tools/terminal/shell.test.ts +29 -0
- package/src/tools/terminal/shell.ts +13 -7
- package/src/util/browser-human-verification.ts +14 -0
- package/src/util/host-process.test.ts +17 -1
- package/src/util/host-process.ts +24 -0
- package/src/util/unicode.ts +29 -0
|
@@ -15,6 +15,13 @@ import type { ToolContext } from "../types.js";
|
|
|
15
15
|
// `mock.module` is hoisted by bun before any static import of the tool
|
|
16
16
|
// runs, so the import below sees the stubbed prompter even though
|
|
17
17
|
// `askQuestionTool` captures the symbol at module-eval time.
|
|
18
|
+
const finishDesktopHelp = mock(async (_resume: boolean) => {});
|
|
19
|
+
const prepareDesktopHelp = mock(
|
|
20
|
+
async (_context: ToolContext) => finishDesktopHelp,
|
|
21
|
+
);
|
|
22
|
+
let promptError: Error | undefined;
|
|
23
|
+
mock.module("../../desktop/desktop-help.js", () => ({ prepareDesktopHelp }));
|
|
24
|
+
|
|
18
25
|
const calls: QuestionPromptParams[] = [];
|
|
19
26
|
let nextResult: QuestionPromptResult = {
|
|
20
27
|
entries: [{ questionId: "q1", decision: "skipped" }],
|
|
@@ -28,6 +35,9 @@ mock.module("../../permissions/question-prompter.js", () => ({
|
|
|
28
35
|
QuestionPrompter: class {
|
|
29
36
|
async prompt(params: QuestionPromptParams): Promise<QuestionPromptOutcome> {
|
|
30
37
|
calls.push(params);
|
|
38
|
+
if (promptError) {
|
|
39
|
+
throw promptError;
|
|
40
|
+
}
|
|
31
41
|
// Mirror the real prompter: it mints the request id and assigns the
|
|
32
42
|
// per-question `q1..qN` ids, then returns them alongside the resolution.
|
|
33
43
|
return {
|
|
@@ -61,6 +71,9 @@ function makeContext(overrides: Partial<ToolContext> = {}): ToolContext {
|
|
|
61
71
|
// `setNextResult()` before invoking `askQuestionTool.execute(...)`.
|
|
62
72
|
beforeEach(() => {
|
|
63
73
|
calls.length = 0;
|
|
74
|
+
prepareDesktopHelp.mockClear();
|
|
75
|
+
finishDesktopHelp.mockClear();
|
|
76
|
+
promptError = undefined;
|
|
64
77
|
nextResult = {
|
|
65
78
|
entries: [{ questionId: "q1", decision: "skipped" }],
|
|
66
79
|
overall: "completed",
|
|
@@ -100,7 +113,7 @@ describe("askQuestionTool definition", () => {
|
|
|
100
113
|
expect(def.description).toContain("up to 5");
|
|
101
114
|
expect(def.description).toContain("Skip button");
|
|
102
115
|
|
|
103
|
-
const schema = def.input_schema as {
|
|
116
|
+
const schema = def.input_schema as unknown as {
|
|
104
117
|
properties: Record<
|
|
105
118
|
string,
|
|
106
119
|
{
|
|
@@ -670,7 +683,7 @@ describe("AskQuestionTool batched input", () => {
|
|
|
670
683
|
});
|
|
671
684
|
|
|
672
685
|
describe("askQuestionTool definition (batched schema)", () => {
|
|
673
|
-
test("exposes
|
|
686
|
+
test("exposes batched questions and a desktop help alternative", () => {
|
|
674
687
|
const def = askQuestionTool;
|
|
675
688
|
const schema = def.input_schema as unknown as {
|
|
676
689
|
properties: Record<
|
|
@@ -708,9 +721,7 @@ describe("askQuestionTool definition (batched schema)", () => {
|
|
|
708
721
|
|
|
709
722
|
expect(questions?.items?.required).toEqual(["question", "options"]);
|
|
710
723
|
|
|
711
|
-
|
|
712
|
-
expect(schema.required).toEqual(["questions"]);
|
|
713
|
-
expect(Object.keys(schema.properties)).toEqual(["questions"]);
|
|
724
|
+
expect(schema.properties.desktopHelp?.type).toBe("object");
|
|
714
725
|
|
|
715
726
|
// The legacy flat fields are gone.
|
|
716
727
|
expect(schema.properties.question).toBeUndefined();
|
|
@@ -825,3 +836,154 @@ describe("answered-question record", () => {
|
|
|
825
836
|
).toBeUndefined();
|
|
826
837
|
});
|
|
827
838
|
});
|
|
839
|
+
|
|
840
|
+
describe("virtual desktop help", () => {
|
|
841
|
+
test("reserves the desktop before waiting and resumes control after Done", async () => {
|
|
842
|
+
setNextResult(singleCompleted({ decision: "option", optionId: "done" }));
|
|
843
|
+
const result = await askQuestionTool.execute(
|
|
844
|
+
{
|
|
845
|
+
desktopHelp: {
|
|
846
|
+
message: "Please complete the CAPTCHA.",
|
|
847
|
+
doneLabel: "Done",
|
|
848
|
+
skipLabel: "Skip",
|
|
849
|
+
},
|
|
850
|
+
},
|
|
851
|
+
makeContext(),
|
|
852
|
+
);
|
|
853
|
+
expect(prepareDesktopHelp).toHaveBeenCalledTimes(1);
|
|
854
|
+
expect(finishDesktopHelp).toHaveBeenCalledWith(true);
|
|
855
|
+
expect(calls[0]?.questions[0]?.presentation).toBe("virtual_desktop");
|
|
856
|
+
expect(result.content).toContain("fresh browser snapshot");
|
|
857
|
+
expect(result.answeredQuestion?.responses[0]).toEqual({
|
|
858
|
+
questionId: "q1",
|
|
859
|
+
decision: "option",
|
|
860
|
+
optionId: "done",
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
test.each([
|
|
865
|
+
{ decision: "skipped" as const },
|
|
866
|
+
{ decision: "option" as const, optionId: "skip" },
|
|
867
|
+
])("does not claim the obstacle was solved after Skip: %j", async (entry) => {
|
|
868
|
+
setNextResult(singleCompleted(entry));
|
|
869
|
+
const result = await askQuestionTool.execute(
|
|
870
|
+
{
|
|
871
|
+
desktopHelp: {
|
|
872
|
+
message: "Please complete the CAPTCHA.",
|
|
873
|
+
doneLabel: "Done",
|
|
874
|
+
skipLabel: "Skip",
|
|
875
|
+
},
|
|
876
|
+
},
|
|
877
|
+
makeContext(),
|
|
878
|
+
);
|
|
879
|
+
expect(finishDesktopHelp).toHaveBeenCalledWith(false);
|
|
880
|
+
expect(result.content).toContain("obstacle may still be present");
|
|
881
|
+
expect(result.isError).toBe(false);
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
test("does not wait for a user in a background turn", async () => {
|
|
885
|
+
await askQuestionTool.execute(
|
|
886
|
+
{
|
|
887
|
+
desktopHelp: {
|
|
888
|
+
message: "Please complete the CAPTCHA.",
|
|
889
|
+
doneLabel: "Done",
|
|
890
|
+
skipLabel: "Skip",
|
|
891
|
+
},
|
|
892
|
+
},
|
|
893
|
+
makeContext({ isInteractive: false }),
|
|
894
|
+
);
|
|
895
|
+
expect(prepareDesktopHelp).not.toHaveBeenCalled();
|
|
896
|
+
expect(calls).toHaveLength(0);
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
test("rejects mixing desktop help with a question batch", async () => {
|
|
900
|
+
const result = await askQuestionTool.execute(
|
|
901
|
+
{
|
|
902
|
+
...validInput,
|
|
903
|
+
desktopHelp: {
|
|
904
|
+
message: "Please complete the CAPTCHA.",
|
|
905
|
+
doneLabel: "Done",
|
|
906
|
+
skipLabel: "Skip",
|
|
907
|
+
},
|
|
908
|
+
},
|
|
909
|
+
makeContext(),
|
|
910
|
+
);
|
|
911
|
+
expect(result.isError).toBe(true);
|
|
912
|
+
expect(calls).toHaveLength(0);
|
|
913
|
+
});
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
test("desktop help preserves model-localized fallback labels in history", async () => {
|
|
917
|
+
setNextResult(singleCompleted({ decision: "option", optionId: "done" }));
|
|
918
|
+
const result = await askQuestionTool.execute(
|
|
919
|
+
{
|
|
920
|
+
desktopHelp: {
|
|
921
|
+
message: "Completa la verificación.",
|
|
922
|
+
doneLabel: "Listo",
|
|
923
|
+
skipLabel: "Omitir",
|
|
924
|
+
},
|
|
925
|
+
},
|
|
926
|
+
makeContext(),
|
|
927
|
+
);
|
|
928
|
+
expect(calls[0]?.questions[0]?.options).toEqual([
|
|
929
|
+
{ id: "done", label: "Listo" },
|
|
930
|
+
{ id: "skip", label: "Omitir" },
|
|
931
|
+
]);
|
|
932
|
+
expect(result.answeredQuestion?.questions[0]?.options).toEqual(
|
|
933
|
+
calls[0]?.questions[0]?.options,
|
|
934
|
+
);
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
test("desktop help does not park on a guardian channel without desktop controls", async () => {
|
|
938
|
+
const result = await askQuestionTool.execute(
|
|
939
|
+
{
|
|
940
|
+
desktopHelp: {
|
|
941
|
+
message: "Please sign in.",
|
|
942
|
+
doneLabel: "Done",
|
|
943
|
+
skipLabel: "Skip",
|
|
944
|
+
},
|
|
945
|
+
},
|
|
946
|
+
makeContext({
|
|
947
|
+
supportsDynamicUi: false,
|
|
948
|
+
supportsGuardianQuestionCards: true,
|
|
949
|
+
}),
|
|
950
|
+
);
|
|
951
|
+
expect(result.isError).toBe(true);
|
|
952
|
+
expect(result.content).toContain("continue in the Vellum app");
|
|
953
|
+
expect(prepareDesktopHelp).not.toHaveBeenCalled();
|
|
954
|
+
expect(calls).toHaveLength(0);
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
for (const overall of ["closed", "timed_out", "aborted"] as const) {
|
|
958
|
+
test(`desktop reservation ends when the question is ${overall}`, async () => {
|
|
959
|
+
setNextResult({ overall, entries: [] });
|
|
960
|
+
await askQuestionTool.execute(
|
|
961
|
+
{
|
|
962
|
+
desktopHelp: {
|
|
963
|
+
message: "Complete verification.",
|
|
964
|
+
doneLabel: "Done",
|
|
965
|
+
skipLabel: "Skip",
|
|
966
|
+
},
|
|
967
|
+
},
|
|
968
|
+
makeContext(),
|
|
969
|
+
);
|
|
970
|
+
expect(finishDesktopHelp).toHaveBeenCalledWith(false);
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
test("desktop reservation ends when presenting the question throws", async () => {
|
|
975
|
+
promptError = new Error("delivery failed");
|
|
976
|
+
await expect(
|
|
977
|
+
askQuestionTool.execute(
|
|
978
|
+
{
|
|
979
|
+
desktopHelp: {
|
|
980
|
+
message: "Complete verification.",
|
|
981
|
+
doneLabel: "Done",
|
|
982
|
+
skipLabel: "Skip",
|
|
983
|
+
},
|
|
984
|
+
},
|
|
985
|
+
makeContext(),
|
|
986
|
+
),
|
|
987
|
+
).rejects.toThrow("delivery failed");
|
|
988
|
+
expect(finishDesktopHelp).toHaveBeenCalledWith(false);
|
|
989
|
+
});
|
|
@@ -7,8 +7,10 @@ import type {
|
|
|
7
7
|
import {
|
|
8
8
|
QuestionPrompter,
|
|
9
9
|
type QuestionPromptOutcome,
|
|
10
|
+
type QuestionPromptParamsEntry,
|
|
10
11
|
} from "../../permissions/question-prompter.js";
|
|
11
12
|
import { RiskLevel } from "../../permissions/types.js";
|
|
13
|
+
import { DESKTOP_HELP_GUIDANCE } from "../../util/browser-human-verification.js";
|
|
12
14
|
import {
|
|
13
15
|
invalidToolInputResult,
|
|
14
16
|
toToolInputSchema,
|
|
@@ -74,17 +76,43 @@ const MAX_QUESTIONS_PER_BATCH = 5;
|
|
|
74
76
|
// Callers pass a (possibly single-element) batch of questions. `execute()`
|
|
75
77
|
// forwards them straight to the prompter. Loose so injected fields (e.g.
|
|
76
78
|
// `activity`) never fail validation.
|
|
77
|
-
export const askQuestionInputSchema = z
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
79
|
+
export const askQuestionInputSchema = z
|
|
80
|
+
.looseObject({
|
|
81
|
+
desktopHelp: z
|
|
82
|
+
.object({
|
|
83
|
+
message: z
|
|
84
|
+
.string()
|
|
85
|
+
.min(1)
|
|
86
|
+
.describe(
|
|
87
|
+
"Use one short sentence to explain the needed human action, in the user's language.",
|
|
88
|
+
),
|
|
89
|
+
doneLabel: z
|
|
90
|
+
.string()
|
|
91
|
+
.min(1)
|
|
92
|
+
.describe("The label for Done in the user's language."),
|
|
93
|
+
skipLabel: z
|
|
94
|
+
.string()
|
|
95
|
+
.min(1)
|
|
96
|
+
.describe("The label for Skip in the user's language."),
|
|
97
|
+
})
|
|
98
|
+
.describe(
|
|
99
|
+
"Immediately request human interaction for any CAPTCHA or bot-detection challenge, including sliders and press-and-hold checks. Also use for native dialogs requiring user interaction. For logins, use saved credentials or securely prompt for missing credentials first. Explain what the user should do. Shows a live preview with Step In, Done and Skip. Pass this instead of questions.",
|
|
100
|
+
)
|
|
101
|
+
.optional(),
|
|
102
|
+
questions: z
|
|
103
|
+
.array(SingleQuestionSchema)
|
|
104
|
+
.min(1)
|
|
105
|
+
.max(MAX_QUESTIONS_PER_BATCH, {
|
|
106
|
+
message: `At most ${MAX_QUESTIONS_PER_BATCH} questions per batch; split into multiple turns if you need more.`,
|
|
107
|
+
})
|
|
108
|
+
.describe(
|
|
109
|
+
`1–${MAX_QUESTIONS_PER_BATCH} clarifying questions to ask in a single turn. Use a batch when several independent ambiguities block progress; ask one at a time when they're sequentially dependent. Past ${MAX_QUESTIONS_PER_BATCH} questions you should be implementing, not asking.`,
|
|
110
|
+
)
|
|
111
|
+
.optional(),
|
|
112
|
+
})
|
|
113
|
+
.refine((input) => Boolean(input.questions) !== Boolean(input.desktopHelp), {
|
|
114
|
+
message: "Provide either questions or desktopHelp, not both.",
|
|
115
|
+
});
|
|
88
116
|
|
|
89
117
|
export type SingleQuestion = z.infer<typeof SingleQuestionSchema>;
|
|
90
118
|
export type AskQuestionInput = z.infer<typeof askQuestionInputSchema>;
|
|
@@ -92,6 +120,10 @@ export type AskQuestionInput = z.infer<typeof askQuestionInputSchema>;
|
|
|
92
120
|
// ── Tool description ────────────────────────────────────────────────
|
|
93
121
|
|
|
94
122
|
const DESCRIPTION = [
|
|
123
|
+
DESKTOP_HELP_GUIDANCE,
|
|
124
|
+
"For logins, use saved credentials first. Securely collect missing credentials",
|
|
125
|
+
"with assistant credentials prompt, then fill the login form yourself.",
|
|
126
|
+
"",
|
|
95
127
|
"Use this tool whenever a request is ambiguous and can be resolved",
|
|
96
128
|
"by 2–4 plausible interpretations or discrete choices. Prefer it over",
|
|
97
129
|
"plain-text clarification — structured options are faster to answer and",
|
|
@@ -223,7 +255,10 @@ export const askQuestionTool = {
|
|
|
223
255
|
category: "interaction",
|
|
224
256
|
executionTarget: "sandbox",
|
|
225
257
|
defaultRiskLevel: RiskLevel.Low,
|
|
226
|
-
input_schema:
|
|
258
|
+
input_schema: {
|
|
259
|
+
...toToolInputSchema(askQuestionInputSchema),
|
|
260
|
+
oneOf: [{ required: ["questions"] }, { required: ["desktopHelp"] }],
|
|
261
|
+
},
|
|
227
262
|
|
|
228
263
|
async execute(
|
|
229
264
|
input: Record<string, unknown>,
|
|
@@ -234,7 +269,19 @@ export const askQuestionTool = {
|
|
|
234
269
|
return invalidToolInputResult("ask_question", parsed.error);
|
|
235
270
|
}
|
|
236
271
|
|
|
237
|
-
const
|
|
272
|
+
const { desktopHelp } = parsed.data;
|
|
273
|
+
const questions: QuestionPromptParamsEntry[] = desktopHelp
|
|
274
|
+
? [
|
|
275
|
+
{
|
|
276
|
+
question: desktopHelp.message,
|
|
277
|
+
options: [
|
|
278
|
+
{ id: "done", label: desktopHelp.doneLabel },
|
|
279
|
+
{ id: "skip", label: desktopHelp.skipLabel },
|
|
280
|
+
],
|
|
281
|
+
presentation: "virtual_desktop",
|
|
282
|
+
},
|
|
283
|
+
]
|
|
284
|
+
: parsed.data.questions!;
|
|
238
285
|
|
|
239
286
|
// No interactive user is present to answer (scheduled/headless/background
|
|
240
287
|
// turn). Don't park the turn on a prompt no one can resolve — proceed with
|
|
@@ -243,12 +290,21 @@ export const askQuestionTool = {
|
|
|
243
290
|
// model asks anyway, so it doesn't wait out the full response timeout.
|
|
244
291
|
if (context.isInteractive === false) {
|
|
245
292
|
return {
|
|
246
|
-
content:
|
|
247
|
-
"No interactive user is present to
|
|
293
|
+
content: desktopHelp
|
|
294
|
+
? "No interactive user is present to help in the virtual desktop. The obstacle remains unresolved."
|
|
295
|
+
: "No interactive user is present to answer; proceeding with reasonable defaults.",
|
|
248
296
|
isError: false,
|
|
249
297
|
};
|
|
250
298
|
}
|
|
251
299
|
|
|
300
|
+
if (desktopHelp && context.supportsDynamicUi === false) {
|
|
301
|
+
return {
|
|
302
|
+
content:
|
|
303
|
+
"This channel cannot show the virtual desktop preview or Step In. Ask the user to continue in the Vellum app to complete this step. The obstacle remains unresolved.",
|
|
304
|
+
isError: true,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
252
308
|
// Channel turns (no dynamic UI) park only when the question can reach the
|
|
253
309
|
// user as a guardian-request card with tappable options: a single-question
|
|
254
310
|
// batch, asked by the guardian, on a channel whose notification adapter
|
|
@@ -277,13 +333,35 @@ export const askQuestionTool = {
|
|
|
277
333
|
};
|
|
278
334
|
}
|
|
279
335
|
|
|
336
|
+
let finishDesktopHelp: ((resume: boolean) => Promise<void>) | undefined;
|
|
337
|
+
if (desktopHelp) {
|
|
338
|
+
const { prepareDesktopHelp } =
|
|
339
|
+
await import("../../desktop/desktop-help.js");
|
|
340
|
+
const prepared = await prepareDesktopHelp(context);
|
|
341
|
+
if (typeof prepared !== "function") {
|
|
342
|
+
return prepared;
|
|
343
|
+
}
|
|
344
|
+
finishDesktopHelp = prepared;
|
|
345
|
+
}
|
|
346
|
+
|
|
280
347
|
const prompter = new QuestionPrompter();
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
348
|
+
let result: QuestionPromptOutcome;
|
|
349
|
+
let resumeDesktop = false;
|
|
350
|
+
try {
|
|
351
|
+
result = await prompter.prompt({
|
|
352
|
+
conversationId: context.conversationId,
|
|
353
|
+
questions,
|
|
354
|
+
toolUseId: context.toolUseId,
|
|
355
|
+
signal: context.signal,
|
|
356
|
+
});
|
|
357
|
+
const answer = result.entries[0];
|
|
358
|
+
resumeDesktop =
|
|
359
|
+
result.overall === "completed" &&
|
|
360
|
+
(answer?.decision === "free_text" ||
|
|
361
|
+
(answer?.decision === "option" && answer.optionId === "done"));
|
|
362
|
+
} finally {
|
|
363
|
+
await finishDesktopHelp?.(resumeDesktop);
|
|
364
|
+
}
|
|
287
365
|
|
|
288
366
|
// Format the aggregated transcript. Each line is keyed by the original
|
|
289
367
|
// question text (not the daemon-assigned id) — the LLM never sees those
|
|
@@ -308,8 +386,26 @@ export const askQuestionTool = {
|
|
|
308
386
|
const answeredQuestion = toAnsweredQuestion(result);
|
|
309
387
|
|
|
310
388
|
switch (result.overall) {
|
|
311
|
-
case "completed":
|
|
312
|
-
|
|
389
|
+
case "completed": {
|
|
390
|
+
let content = lines.join("\n");
|
|
391
|
+
if (desktopHelp) {
|
|
392
|
+
const entry = result.entries[0];
|
|
393
|
+
if (entry?.decision === "free_text") {
|
|
394
|
+
content +=
|
|
395
|
+
"\nTake a fresh browser snapshot before continuing; do not assume the obstacle was resolved.";
|
|
396
|
+
} else if (
|
|
397
|
+
entry?.decision === "option" &&
|
|
398
|
+
entry.optionId === "done"
|
|
399
|
+
) {
|
|
400
|
+
content =
|
|
401
|
+
"The user finished interacting with the virtual desktop. Take a fresh browser snapshot and verify the result before continuing.";
|
|
402
|
+
} else {
|
|
403
|
+
content =
|
|
404
|
+
"The user skipped helping in the virtual desktop. The obstacle may still be present. Use another approach or explain what remains blocked.";
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return { content, isError: false, answeredQuestion };
|
|
408
|
+
}
|
|
313
409
|
case "closed": {
|
|
314
410
|
const summary =
|
|
315
411
|
"User closed the question card without answering. All questions skipped.";
|
|
@@ -4,6 +4,10 @@ import { getConfig } from "../../config/loader.js";
|
|
|
4
4
|
import { HostBrowserProxy } from "../../daemon/host-browser-proxy.js";
|
|
5
5
|
import type { ImageContent } from "../../providers/types.js";
|
|
6
6
|
import { wrapUntrustedContent } from "../../security/untrusted-content.js";
|
|
7
|
+
import {
|
|
8
|
+
DESKTOP_HELP_GUIDANCE,
|
|
9
|
+
HUMAN_VERIFICATION_GUIDANCE,
|
|
10
|
+
} from "../../util/browser-human-verification.js";
|
|
7
11
|
import { getLogger } from "../../util/logger.js";
|
|
8
12
|
import { truncate } from "../../util/truncate.js";
|
|
9
13
|
import { safeStringSlice } from "../../util/unicode.js";
|
|
@@ -1319,7 +1323,9 @@ export async function executeBrowserNavigate(
|
|
|
1319
1323
|
"⚠️ CAPTCHA/Cloudflare verification detected on this page.",
|
|
1320
1324
|
);
|
|
1321
1325
|
lines.push(
|
|
1322
|
-
|
|
1326
|
+
context.cdpClient
|
|
1327
|
+
? DESKTOP_HELP_GUIDANCE
|
|
1328
|
+
: HUMAN_VERIFICATION_GUIDANCE,
|
|
1323
1329
|
);
|
|
1324
1330
|
if (cdp.kind === "local") {
|
|
1325
1331
|
lines.push("");
|
|
@@ -23,8 +23,10 @@ import { conversationRevealNonce } from "../../runtime/reveal-nonce.js";
|
|
|
23
23
|
import { redactSecrets } from "../../security/secret-scanner.js";
|
|
24
24
|
import {
|
|
25
25
|
buildShellInvocation,
|
|
26
|
+
buildShellSpawnFlags,
|
|
26
27
|
prependUniquePathEntries,
|
|
27
28
|
terminateProcessTree,
|
|
29
|
+
watchShellProcessStart,
|
|
28
30
|
} from "../../util/host-process.js";
|
|
29
31
|
import { getLogger } from "../../util/logger.js";
|
|
30
32
|
import type { CompletedBackgroundTool } from "../background-tool-registry.js";
|
|
@@ -455,9 +457,9 @@ export const hostShellTool = {
|
|
|
455
457
|
cwd: workingDir,
|
|
456
458
|
env: hostEnv,
|
|
457
459
|
stdio: ["ignore", "pipe", "pipe"],
|
|
458
|
-
|
|
459
|
-
windowsHide: true,
|
|
460
|
+
...buildShellSpawnFlags(),
|
|
460
461
|
});
|
|
462
|
+
const launch = watchShellProcessStart(child);
|
|
461
463
|
|
|
462
464
|
const collector = attachBoundedStdio(child);
|
|
463
465
|
let timedOut = false;
|
|
@@ -483,7 +485,9 @@ export const hostShellTool = {
|
|
|
483
485
|
}
|
|
484
486
|
completed = true;
|
|
485
487
|
clearTimeout(timer);
|
|
486
|
-
const result = collector.format(code, timedOut, timeoutSec
|
|
488
|
+
const result = collector.format(code, timedOut, timeoutSec, {
|
|
489
|
+
started: launch.didStart(),
|
|
490
|
+
});
|
|
487
491
|
// Cancel takes precedence over the SIGKILL-induced error result.
|
|
488
492
|
const status = aborted
|
|
489
493
|
? "cancelled"
|
|
@@ -629,9 +633,9 @@ export const hostShellTool = {
|
|
|
629
633
|
cwd: workingDir,
|
|
630
634
|
env: hostEnv,
|
|
631
635
|
stdio: ["ignore", "pipe", "pipe"],
|
|
632
|
-
|
|
633
|
-
windowsHide: true,
|
|
636
|
+
...buildShellSpawnFlags(),
|
|
634
637
|
});
|
|
638
|
+
const launch = watchShellProcessStart(child);
|
|
635
639
|
const collector = attachBoundedStdio(child, {
|
|
636
640
|
onOutput: context.onOutput,
|
|
637
641
|
});
|
|
@@ -657,7 +661,9 @@ export const hostShellTool = {
|
|
|
657
661
|
clearTimeout(timer);
|
|
658
662
|
context.signal?.removeEventListener("abort", onAbort);
|
|
659
663
|
|
|
660
|
-
const result = collector.format(code, timedOut, timeoutSec
|
|
664
|
+
const result = collector.format(code, timedOut, timeoutSec, {
|
|
665
|
+
started: launch.didStart(),
|
|
666
|
+
});
|
|
661
667
|
|
|
662
668
|
resolve({
|
|
663
669
|
content: result.content,
|
|
@@ -4,6 +4,7 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import { minimatch } from "minimatch";
|
|
5
5
|
|
|
6
6
|
import { ensureDir, pathExists } from "../../../util/fs.js";
|
|
7
|
+
import { surrogateSafeWindow } from "../../../util/unicode.js";
|
|
7
8
|
import { isAbortLikeError } from "../abort.js";
|
|
8
9
|
import { applyEdit } from "./edit-engine.js";
|
|
9
10
|
import * as Err from "./errors.js";
|
|
@@ -102,37 +103,6 @@ function truncationNotice(
|
|
|
102
103
|
return `\n\n[Truncated: characters ${start}-${end} of ${totalChars}. Read on with start_index=${end}.]`;
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
const isHighSurrogate = (code: number): boolean =>
|
|
106
|
-
code >= 0xd800 && code <= 0xdbff;
|
|
107
|
-
const isLowSurrogate = (code: number): boolean =>
|
|
108
|
-
code >= 0xdc00 && code <= 0xdfff;
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Character window that never splits a surrogate pair. A split leaves a lone
|
|
112
|
-
* half at each edge, and each encodes to U+FFFD, so the character is lost from
|
|
113
|
-
* both this window and the next one paged in after it.
|
|
114
|
-
*/
|
|
115
|
-
export function surrogateSafeWindow(
|
|
116
|
-
total: number,
|
|
117
|
-
charCodeAt: (index: number) => number,
|
|
118
|
-
requestedStart: number,
|
|
119
|
-
maxChars: number,
|
|
120
|
-
): { start: number; end: number } {
|
|
121
|
-
let start = Math.max(0, Math.min(requestedStart, total));
|
|
122
|
-
if (start > 0 && start < total && isLowSurrogate(charCodeAt(start))) {
|
|
123
|
-
start -= 1;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
let end = Math.min(total, start + maxChars);
|
|
127
|
-
if (end > start && end < total && isHighSurrogate(charCodeAt(end - 1))) {
|
|
128
|
-
// Backing off would empty a one-character window, which stalls paging on
|
|
129
|
-
// the same offset, so take the whole pair instead.
|
|
130
|
-
end = end - 1 > start ? end - 1 : Math.min(total, end + 1);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return { start, end };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
106
|
export class FileSystemOps {
|
|
137
107
|
private policy: PathPolicy;
|
|
138
108
|
private sizeLimit: number | undefined;
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
formatShellOutput,
|
|
8
8
|
MAX_OUTPUT_LENGTH,
|
|
9
9
|
OUTPUT_TRUNCATED_TAG,
|
|
10
|
+
SHELL_DID_NOT_START_MESSAGE,
|
|
10
11
|
} from "./shell-output.js";
|
|
11
12
|
|
|
12
13
|
describe("BoundedStdioCollector", () => {
|
|
@@ -85,6 +86,15 @@ describe("attachBoundedStdio", () => {
|
|
|
85
86
|
});
|
|
86
87
|
});
|
|
87
88
|
|
|
89
|
+
describe("formatShellOutput launch start", () => {
|
|
90
|
+
test("does not report command_completed when the process never started", () => {
|
|
91
|
+
const result = formatShellOutput("", "", 0, false, 120, { started: false });
|
|
92
|
+
expect(result.isError).toBe(true);
|
|
93
|
+
expect(result.content).toBe(SHELL_DID_NOT_START_MESSAGE);
|
|
94
|
+
expect(result.content).not.toContain("<command_completed />");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
88
98
|
describe("formatShellOutput truncation", () => {
|
|
89
99
|
test("truncates an already-materialized oversized string without a file path", () => {
|
|
90
100
|
const longOutput = "x".repeat(30_000);
|
|
@@ -4,6 +4,9 @@ export const MAX_OUTPUT_LENGTH = 20_000;
|
|
|
4
4
|
|
|
5
5
|
export const OUTPUT_TRUNCATED_TAG = `<output_truncated limit="20K" />`;
|
|
6
6
|
|
|
7
|
+
export const SHELL_DID_NOT_START_MESSAGE =
|
|
8
|
+
"Error: the shell command did not start. No process was created, so the command did not run.";
|
|
9
|
+
|
|
7
10
|
export interface ShellOutputResult {
|
|
8
11
|
content: string;
|
|
9
12
|
status: string | undefined;
|
|
@@ -31,8 +34,16 @@ export function formatShellOutput(
|
|
|
31
34
|
code: number | null,
|
|
32
35
|
timedOut: boolean,
|
|
33
36
|
timeoutSec: number,
|
|
34
|
-
options?: { truncated?: boolean },
|
|
37
|
+
options?: { truncated?: boolean; started?: boolean },
|
|
35
38
|
): ShellOutputResult {
|
|
39
|
+
if (options?.started === false) {
|
|
40
|
+
return {
|
|
41
|
+
content: SHELL_DID_NOT_START_MESSAGE,
|
|
42
|
+
status: undefined,
|
|
43
|
+
isError: true,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
36
47
|
let output = stdout;
|
|
37
48
|
if (stderr) {
|
|
38
49
|
output += (output ? "\n" : "") + stderr;
|
|
@@ -137,6 +148,7 @@ export class BoundedStdioCollector {
|
|
|
137
148
|
code: number | null,
|
|
138
149
|
timedOut: boolean,
|
|
139
150
|
timeoutSec: number,
|
|
151
|
+
options?: { started?: boolean },
|
|
140
152
|
): ShellOutputResult {
|
|
141
153
|
return formatShellOutput(
|
|
142
154
|
Buffer.concat(this.stdoutParts).toString(),
|
|
@@ -144,7 +156,7 @@ export class BoundedStdioCollector {
|
|
|
144
156
|
code,
|
|
145
157
|
timedOut,
|
|
146
158
|
timeoutSec,
|
|
147
|
-
{ truncated: this.truncated },
|
|
159
|
+
{ truncated: this.truncated, started: options?.started },
|
|
148
160
|
);
|
|
149
161
|
}
|
|
150
162
|
}
|
|
@@ -7,9 +7,12 @@ import { conversationRevealNonce } from "../../runtime/reveal-nonce.js";
|
|
|
7
7
|
import { computeSkillVersionHash } from "../../skills/version-hash.js";
|
|
8
8
|
import {
|
|
9
9
|
buildShellInvocation,
|
|
10
|
+
buildShellSpawnFlags,
|
|
10
11
|
terminateProcessTree,
|
|
12
|
+
watchShellProcessStart,
|
|
11
13
|
} from "../../util/host-process.js";
|
|
12
14
|
import { safeStringSlice } from "../../util/unicode.js";
|
|
15
|
+
import { SHELL_DID_NOT_START_MESSAGE } from "../shared/shell-output.js";
|
|
13
16
|
import { buildSanitizedEnv } from "../terminal/safe-env.js";
|
|
14
17
|
import type { ToolContext, ToolExecutionResult } from "../types.js";
|
|
15
18
|
|
|
@@ -160,9 +163,9 @@ function spawnRunner(
|
|
|
160
163
|
cwd: runDir,
|
|
161
164
|
env,
|
|
162
165
|
stdio: ["ignore", "pipe", "pipe"],
|
|
163
|
-
|
|
164
|
-
windowsHide: true,
|
|
166
|
+
...buildShellSpawnFlags(),
|
|
165
167
|
});
|
|
168
|
+
const launch = watchShellProcessStart(child);
|
|
166
169
|
|
|
167
170
|
const timer = setTimeout(() => {
|
|
168
171
|
timedOut = true;
|
|
@@ -188,6 +191,14 @@ function spawnRunner(
|
|
|
188
191
|
clearTimeout(timer);
|
|
189
192
|
context.signal?.removeEventListener("abort", onAbort);
|
|
190
193
|
|
|
194
|
+
if (!launch.didStart()) {
|
|
195
|
+
resolve({
|
|
196
|
+
content: `Failed to spawn skill tool script "${executorPath}": ${SHELL_DID_NOT_START_MESSAGE}`,
|
|
197
|
+
isError: true,
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
191
202
|
if (timedOut) {
|
|
192
203
|
resolve({
|
|
193
204
|
content: `Skill tool script "${executorPath}" timed out after ${timeoutMs}ms`,
|
|
@@ -134,7 +134,8 @@ function notifyBackgroundSkillUpdate(args: {
|
|
|
134
134
|
sourceEventName: "activity.complete",
|
|
135
135
|
dedupeKey: `skill-updated:${args.skillId}:${day}`,
|
|
136
136
|
contextPayload: {
|
|
137
|
-
// `summary` feeds the copy composer
|
|
137
|
+
// `summary` feeds the copy composer and `title` is the headline both
|
|
138
|
+
// it and the home feed keep; `title`/`body` are also the feed's
|
|
138
139
|
// fallback when no channel copy was rendered. Without them a fully
|
|
139
140
|
// suppressed delivery (the intended shape for this signal: low urgency,
|
|
140
141
|
// background, no interruption) leaves the feed writer with no summary
|