@namzu/sdk 3.0.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +223 -0
- package/dist/advisory/__tests__/consultation-context.test.d.ts +2 -0
- package/dist/advisory/__tests__/consultation-context.test.d.ts.map +1 -0
- package/dist/advisory/__tests__/consultation-context.test.js +124 -0
- package/dist/advisory/__tests__/consultation-context.test.js.map +1 -0
- package/dist/advisory/context.d.ts +25 -0
- package/dist/advisory/context.d.ts.map +1 -1
- package/dist/advisory/context.js +18 -0
- package/dist/advisory/context.js.map +1 -1
- package/dist/advisory/executor.d.ts.map +1 -1
- package/dist/advisory/executor.js +25 -3
- package/dist/advisory/executor.js.map +1 -1
- package/dist/compaction/__tests__/context-reducer.test.d.ts +2 -0
- package/dist/compaction/__tests__/context-reducer.test.d.ts.map +1 -0
- package/dist/compaction/__tests__/context-reducer.test.js +197 -0
- package/dist/compaction/__tests__/context-reducer.test.js.map +1 -0
- package/dist/compaction/factory.d.ts +7 -0
- package/dist/compaction/factory.d.ts.map +1 -1
- package/dist/compaction/factory.js +7 -0
- package/dist/compaction/factory.js.map +1 -1
- package/dist/compaction/index.d.ts +2 -0
- package/dist/compaction/index.d.ts.map +1 -1
- package/dist/compaction/index.js +1 -0
- package/dist/compaction/index.js.map +1 -1
- package/dist/compaction/interface.d.ts +13 -0
- package/dist/compaction/interface.d.ts.map +1 -1
- package/dist/compaction/managers/null.d.ts +3 -0
- package/dist/compaction/managers/null.d.ts.map +1 -1
- package/dist/compaction/managers/null.js +3 -0
- package/dist/compaction/managers/null.js.map +1 -1
- package/dist/compaction/managers/slidingWindow.d.ts +6 -0
- package/dist/compaction/managers/slidingWindow.d.ts.map +1 -1
- package/dist/compaction/managers/slidingWindow.js +6 -0
- package/dist/compaction/managers/slidingWindow.js.map +1 -1
- package/dist/compaction/managers/structured.d.ts +10 -0
- package/dist/compaction/managers/structured.d.ts.map +1 -1
- package/dist/compaction/managers/structured.js +10 -0
- package/dist/compaction/managers/structured.js.map +1 -1
- package/dist/compaction/reducer.d.ts +86 -0
- package/dist/compaction/reducer.d.ts.map +1 -0
- package/dist/compaction/reducer.js +77 -0
- package/dist/compaction/reducer.js.map +1 -0
- package/dist/connector/builtins/__tests__/oauth2-auth.test.d.ts +2 -0
- package/dist/connector/builtins/__tests__/oauth2-auth.test.d.ts.map +1 -0
- package/dist/connector/builtins/__tests__/oauth2-auth.test.js +54 -0
- package/dist/connector/builtins/__tests__/oauth2-auth.test.js.map +1 -0
- package/dist/connector/builtins/http.d.ts.map +1 -1
- package/dist/connector/builtins/http.js +24 -2
- package/dist/connector/builtins/http.js.map +1 -1
- package/dist/connector/builtins/http.test.js +18 -2
- package/dist/connector/builtins/http.test.js.map +1 -1
- package/dist/connector/index.d.ts +2 -2
- package/dist/connector/index.d.ts.map +1 -1
- package/dist/connector/index.js +1 -1
- package/dist/connector/index.js.map +1 -1
- package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.d.ts +2 -0
- package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.d.ts.map +1 -0
- package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.js +214 -0
- package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.js.map +1 -0
- package/dist/connector/mcp/client.d.ts +52 -1
- package/dist/connector/mcp/client.d.ts.map +1 -1
- package/dist/connector/mcp/client.js +86 -0
- package/dist/connector/mcp/client.js.map +1 -1
- package/dist/connector/mcp/discovery.d.ts +12 -1
- package/dist/connector/mcp/discovery.d.ts.map +1 -1
- package/dist/connector/mcp/discovery.js +19 -4
- package/dist/connector/mcp/discovery.js.map +1 -1
- package/dist/connector/mcp/index.d.ts +2 -2
- package/dist/connector/mcp/index.d.ts.map +1 -1
- package/dist/connector/mcp/index.js +1 -1
- package/dist/connector/mcp/index.js.map +1 -1
- package/dist/connector/mcp/server.d.ts +42 -1
- package/dist/connector/mcp/server.d.ts.map +1 -1
- package/dist/connector/mcp/server.js +77 -4
- package/dist/connector/mcp/server.js.map +1 -1
- package/dist/manager/agent/__tests__/depth-limit-authority.test.d.ts +2 -0
- package/dist/manager/agent/__tests__/depth-limit-authority.test.d.ts.map +1 -0
- package/dist/manager/agent/__tests__/depth-limit-authority.test.js +58 -0
- package/dist/manager/agent/__tests__/depth-limit-authority.test.js.map +1 -0
- package/dist/plugin/__tests__/discovery-scopes.test.d.ts +2 -0
- package/dist/plugin/__tests__/discovery-scopes.test.d.ts.map +1 -0
- package/dist/plugin/__tests__/discovery-scopes.test.js +97 -0
- package/dist/plugin/__tests__/discovery-scopes.test.js.map +1 -0
- package/dist/plugin/__tests__/enable-contributions.test.js +5 -1
- package/dist/plugin/__tests__/enable-contributions.test.js.map +1 -1
- package/dist/plugin/__tests__/mcp-admission.test.d.ts +2 -0
- package/dist/plugin/__tests__/mcp-admission.test.d.ts.map +1 -0
- package/dist/plugin/__tests__/mcp-admission.test.js +192 -0
- package/dist/plugin/__tests__/mcp-admission.test.js.map +1 -0
- package/dist/plugin/lifecycle.d.ts +41 -0
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/lifecycle.js +29 -1
- package/dist/plugin/lifecycle.js.map +1 -1
- package/dist/plugin/loader.d.ts +39 -3
- package/dist/plugin/loader.d.ts.map +1 -1
- package/dist/plugin/loader.js +37 -4
- package/dist/plugin/loader.js.map +1 -1
- package/dist/public-runtime.d.ts +5 -3
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +11 -3
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +2 -2
- package/dist/public-types.d.ts.map +1 -1
- package/dist/rag/__tests__/namespace-isolation.test.d.ts +2 -0
- package/dist/rag/__tests__/namespace-isolation.test.d.ts.map +1 -0
- package/dist/rag/__tests__/namespace-isolation.test.js +80 -0
- package/dist/rag/__tests__/namespace-isolation.test.js.map +1 -0
- package/dist/rag/ingestion.d.ts.map +1 -1
- package/dist/rag/ingestion.js +1 -0
- package/dist/rag/ingestion.js.map +1 -1
- package/dist/rag/retriever.d.ts.map +1 -1
- package/dist/rag/retriever.js +2 -0
- package/dist/rag/retriever.js.map +1 -1
- package/dist/rag/vector-store.d.ts.map +1 -1
- package/dist/rag/vector-store.js +6 -0
- package/dist/rag/vector-store.js.map +1 -1
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +113 -109
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/runtime/query/__tests__/capability-negotiation.test.js +59 -0
- package/dist/runtime/query/__tests__/capability-negotiation.test.js.map +1 -1
- package/dist/runtime/query/__tests__/per-step-skills.test.d.ts +10 -0
- package/dist/runtime/query/__tests__/per-step-skills.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/per-step-skills.test.js +122 -0
- package/dist/runtime/query/__tests__/per-step-skills.test.js.map +1 -0
- package/dist/runtime/query/__tests__/per-step-tool-choice.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/per-step-tool-choice.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/per-step-tool-choice.test.js +153 -0
- package/dist/runtime/query/__tests__/per-step-tool-choice.test.js.map +1 -0
- package/dist/runtime/query/__tests__/resume-run.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/resume-run.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/resume-run.test.js +211 -0
- package/dist/runtime/query/__tests__/resume-run.test.js.map +1 -0
- package/dist/runtime/query/executor.d.ts +1 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +10 -1
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +10 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +26 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +118 -34
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction-model-routing.test.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/compaction-model-routing.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/compaction-model-routing.test.js +96 -0
- package/dist/runtime/query/iteration/phases/compaction-model-routing.test.js.map +1 -0
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +95 -5
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.js +180 -0
- package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.js.map +1 -0
- package/dist/runtime/query/iteration/phases/context.d.ts +9 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/resume-run.d.ts +70 -0
- package/dist/runtime/query/resume-run.d.ts.map +1 -0
- package/dist/runtime/query/resume-run.js +46 -0
- package/dist/runtime/query/resume-run.js.map +1 -0
- package/dist/telemetry/__tests__/model-call-span.test.d.ts +2 -0
- package/dist/telemetry/__tests__/model-call-span.test.d.ts.map +1 -0
- package/dist/telemetry/__tests__/model-call-span.test.js +147 -0
- package/dist/telemetry/__tests__/model-call-span.test.js.map +1 -0
- package/dist/telemetry/__tests__/span-closure.test.d.ts +2 -0
- package/dist/telemetry/__tests__/span-closure.test.d.ts.map +1 -0
- package/dist/telemetry/__tests__/span-closure.test.js +124 -0
- package/dist/telemetry/__tests__/span-closure.test.js.map +1 -0
- package/dist/tools/advisory/index.js +1 -1
- package/dist/tools/advisory/index.js.map +1 -1
- package/dist/tools/builtins/__tests__/edit-commits-atomically.test.d.ts +2 -0
- package/dist/tools/builtins/__tests__/edit-commits-atomically.test.d.ts.map +1 -0
- package/dist/tools/builtins/__tests__/edit-commits-atomically.test.js +78 -0
- package/dist/tools/builtins/__tests__/edit-commits-atomically.test.js.map +1 -0
- package/dist/tools/builtins/__tests__/edit.test.js +242 -1
- package/dist/tools/builtins/__tests__/edit.test.js.map +1 -1
- package/dist/tools/builtins/__tests__/stale-file-refusal.test.d.ts +2 -0
- package/dist/tools/builtins/__tests__/stale-file-refusal.test.d.ts.map +1 -0
- package/dist/tools/builtins/__tests__/stale-file-refusal.test.js +187 -0
- package/dist/tools/builtins/__tests__/stale-file-refusal.test.js.map +1 -0
- package/dist/tools/builtins/atomic-write-file.d.ts +1 -1
- package/dist/tools/builtins/atomic-write-file.d.ts.map +1 -1
- package/dist/tools/builtins/atomic-write-file.js +31 -2
- package/dist/tools/builtins/atomic-write-file.js.map +1 -1
- package/dist/tools/builtins/content-fingerprint.d.ts +23 -0
- package/dist/tools/builtins/content-fingerprint.d.ts.map +1 -0
- package/dist/tools/builtins/content-fingerprint.js +30 -0
- package/dist/tools/builtins/content-fingerprint.js.map +1 -0
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +172 -31
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +5 -2
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/builtins/write-file.d.ts.map +1 -1
- package/dist/tools/builtins/write-file.js +75 -27
- package/dist/tools/builtins/write-file.js.map +1 -1
- package/dist/tools/coordinator/__tests__/ask-user-question.test.js +55 -0
- package/dist/tools/coordinator/__tests__/ask-user-question.test.js.map +1 -1
- package/dist/tools/coordinator/__tests__/plan-dependencies.test.d.ts +2 -0
- package/dist/tools/coordinator/__tests__/plan-dependencies.test.d.ts.map +1 -0
- package/dist/tools/coordinator/__tests__/plan-dependencies.test.js +126 -0
- package/dist/tools/coordinator/__tests__/plan-dependencies.test.js.map +1 -0
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +75 -5
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/tools/coordinator/plan-dependencies.d.ts +43 -0
- package/dist/tools/coordinator/plan-dependencies.d.ts.map +1 -0
- package/dist/tools/coordinator/plan-dependencies.js +148 -0
- package/dist/tools/coordinator/plan-dependencies.js.map +1 -0
- package/dist/types/agent/supervisor.d.ts +15 -0
- package/dist/types/agent/supervisor.d.ts.map +1 -1
- package/dist/types/connector/core.d.ts +34 -0
- package/dist/types/connector/core.d.ts.map +1 -1
- package/dist/types/connector/definition.d.ts +10 -0
- package/dist/types/connector/definition.d.ts.map +1 -1
- package/dist/types/connector/mcp.d.ts +13 -0
- package/dist/types/connector/mcp.d.ts.map +1 -1
- package/dist/types/rag/retrieval.d.ts +16 -0
- package/dist/types/rag/retrieval.d.ts.map +1 -1
- package/dist/types/rag/storage.d.ts +9 -0
- package/dist/types/rag/storage.d.ts.map +1 -1
- package/dist/types/rag/vector.d.ts +11 -0
- package/dist/types/rag/vector.d.ts.map +1 -1
- package/dist/types/router/task-router.d.ts +19 -0
- package/dist/types/router/task-router.d.ts.map +1 -1
- package/dist/types/run/prepare-step.d.ts +56 -3
- package/dist/types/run/prepare-step.d.ts.map +1 -1
- package/dist/types/tool/index.d.ts +17 -1
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/toolset/index.d.ts +22 -0
- package/dist/types/toolset/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/advisory/__tests__/consultation-context.test.ts +191 -0
- package/src/advisory/context.ts +32 -0
- package/src/advisory/executor.ts +30 -3
- package/src/compaction/__tests__/context-reducer.test.ts +239 -0
- package/src/compaction/factory.ts +7 -0
- package/src/compaction/index.ts +8 -0
- package/src/compaction/interface.ts +13 -0
- package/src/compaction/managers/null.ts +3 -0
- package/src/compaction/managers/slidingWindow.ts +6 -0
- package/src/compaction/managers/structured.ts +10 -0
- package/src/compaction/reducer.ts +154 -0
- package/src/connector/builtins/__tests__/oauth2-auth.test.ts +73 -0
- package/src/connector/builtins/http.test.ts +28 -2
- package/src/connector/builtins/http.ts +26 -2
- package/src/connector/index.ts +6 -2
- package/src/connector/mcp/__tests__/prompts-and-lifecycle.test.ts +286 -0
- package/src/connector/mcp/client.ts +95 -0
- package/src/connector/mcp/discovery.ts +19 -4
- package/src/connector/mcp/index.ts +6 -2
- package/src/connector/mcp/server.ts +101 -3
- package/src/manager/agent/__tests__/depth-limit-authority.test.ts +74 -0
- package/src/plugin/__tests__/discovery-scopes.test.ts +133 -0
- package/src/plugin/__tests__/enable-contributions.test.ts +5 -1
- package/src/plugin/__tests__/mcp-admission.test.ts +242 -0
- package/src/plugin/lifecycle.ts +57 -1
- package/src/plugin/loader.ts +57 -3
- package/src/public-runtime.ts +16 -1
- package/src/public-types.ts +5 -0
- package/src/rag/__tests__/namespace-isolation.test.ts +109 -0
- package/src/rag/ingestion.ts +1 -0
- package/src/rag/retriever.ts +2 -0
- package/src/rag/vector-store.ts +5 -0
- package/src/registry/tool/execute.ts +123 -119
- package/src/runtime/query/__tests__/capability-negotiation.test.ts +69 -0
- package/src/runtime/query/__tests__/per-step-skills.test.ts +154 -0
- package/src/runtime/query/__tests__/per-step-tool-choice.test.ts +180 -0
- package/src/runtime/query/__tests__/resume-run.test.ts +262 -0
- package/src/runtime/query/executor.ts +9 -1
- package/src/runtime/query/index.ts +39 -0
- package/src/runtime/query/iteration/index.ts +130 -34
- package/src/runtime/query/iteration/phases/compaction-model-routing.test.ts +125 -0
- package/src/runtime/query/iteration/phases/compaction.ts +106 -5
- package/src/runtime/query/iteration/phases/context-reducer-dispatch.test.ts +238 -0
- package/src/runtime/query/iteration/phases/context.ts +11 -0
- package/src/runtime/query/resume-run.ts +93 -0
- package/src/telemetry/__tests__/model-call-span.test.ts +189 -0
- package/src/telemetry/__tests__/span-closure.test.ts +153 -0
- package/src/tools/advisory/index.ts +1 -1
- package/src/tools/builtins/__tests__/edit-commits-atomically.test.ts +103 -0
- package/src/tools/builtins/__tests__/edit.test.ts +324 -1
- package/src/tools/builtins/__tests__/stale-file-refusal.test.ts +258 -0
- package/src/tools/builtins/atomic-write-file.ts +30 -2
- package/src/tools/builtins/content-fingerprint.ts +33 -0
- package/src/tools/builtins/edit.ts +188 -36
- package/src/tools/builtins/read-file.ts +5 -2
- package/src/tools/builtins/write-file.ts +79 -28
- package/src/tools/coordinator/__tests__/ask-user-question.test.ts +68 -0
- package/src/tools/coordinator/__tests__/plan-dependencies.test.ts +186 -0
- package/src/tools/coordinator/index.ts +116 -42
- package/src/tools/coordinator/plan-dependencies.ts +175 -0
- package/src/types/agent/supervisor.ts +15 -0
- package/src/types/connector/core.ts +34 -0
- package/src/types/connector/definition.ts +10 -0
- package/src/types/connector/mcp.ts +14 -0
- package/src/types/rag/retrieval.ts +16 -0
- package/src/types/rag/storage.ts +9 -0
- package/src/types/rag/vector.ts +11 -0
- package/src/types/router/task-router.ts +19 -0
- package/src/types/run/prepare-step.ts +58 -3
- package/src/types/tool/index.ts +17 -1
- package/src/types/toolset/index.ts +22 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { access, mkdir
|
|
1
|
+
import { access, mkdir } from 'node:fs/promises'
|
|
2
2
|
import { dirname, resolve } from 'node:path'
|
|
3
3
|
import { z } from 'zod'
|
|
4
4
|
import type { ToolContext } from '../../types/tool/index.js'
|
|
5
5
|
import { defineTool } from '../defineTool.js'
|
|
6
|
+
import { atomicWriteFile } from './atomic-write-file.js'
|
|
7
|
+
import { withFileMutationLock } from './file-mutation-lock.js'
|
|
6
8
|
|
|
7
9
|
const inputSchema = z
|
|
8
10
|
.object({
|
|
@@ -25,17 +27,46 @@ const inputSchema = z
|
|
|
25
27
|
'Alias for content. Useful for hosts that expose create/write operations as newStr. Self-budget this payload under 12000 characters before calling.',
|
|
26
28
|
),
|
|
27
29
|
})
|
|
30
|
+
.strict()
|
|
28
31
|
.refine((value) => typeof value.content === 'string' || typeof value.newStr === 'string', {
|
|
29
32
|
message: 'Either content or newStr is required.',
|
|
30
33
|
})
|
|
31
34
|
|
|
32
35
|
type WriteInput = z.infer<typeof inputSchema>
|
|
33
36
|
|
|
37
|
+
/**
|
|
38
|
+
* The single shape a model is constrained to emit.
|
|
39
|
+
*
|
|
40
|
+
* `newStr` is a host affordance and deliberately absent here: a model given
|
|
41
|
+
* two names for the body has to pick, and picking is what produces the
|
|
42
|
+
* half-filled calls this schema exists to prevent.
|
|
43
|
+
*/
|
|
44
|
+
const modelInputSchema: Record<string, unknown> = {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {
|
|
47
|
+
path: {
|
|
48
|
+
type: 'string',
|
|
49
|
+
description: 'Relative path to the file to write. Must not be empty.',
|
|
50
|
+
},
|
|
51
|
+
content: {
|
|
52
|
+
type: 'string',
|
|
53
|
+
description:
|
|
54
|
+
'Complete file body. Use "" only for an intentionally empty file. Keep under 12000 characters.',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
required: ['path', 'content'],
|
|
58
|
+
additionalProperties: false,
|
|
59
|
+
}
|
|
60
|
+
|
|
34
61
|
export const WriteFileTool = defineTool({
|
|
35
62
|
name: 'write',
|
|
36
63
|
description:
|
|
37
64
|
'Writes a file to the local filesystem. Overwrites the existing file at the path if there is one.\n\n- If the file already exists, you must use the `read` tool on it first in this conversation, or this call will fail.\n- Prefer the `edit` tool for modifying existing files — it only sends the diff and preserves the rest of the file byte-for-byte.\n- Use `write` to create a new file or to perform a deliberate full rewrite of a file you have already read.\n- Self-budget content/newStr under 12000 characters before emitting the tool call. For long content, write a smaller opening section, then use `edit` with insertLine: "end" to extend the file section by section. Do not chain multiple `write` calls — each one overwrites the previous.',
|
|
38
65
|
inputSchema,
|
|
66
|
+
modelInputSchema,
|
|
67
|
+
enforceModelInput: true,
|
|
68
|
+
validationErrorHint:
|
|
69
|
+
'Required shape: {"path":"file.md","content":"complete file body"}. Pass the whole body, not a diff.',
|
|
39
70
|
category: 'filesystem',
|
|
40
71
|
permissions: ['file_write'],
|
|
41
72
|
readOnly: false,
|
|
@@ -43,40 +74,60 @@ export const WriteFileTool = defineTool({
|
|
|
43
74
|
concurrencySafe: false,
|
|
44
75
|
|
|
45
76
|
async execute(input: WriteInput, context) {
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const guard = enforceReadBeforeOverwrite(context, input.path)
|
|
52
|
-
if (guard) return guard
|
|
53
|
-
}
|
|
54
|
-
await context.sandbox.writeFile(input.path, content)
|
|
55
|
-
context.fileReadTracker?.recordRead(input.path)
|
|
77
|
+
// `execute` is reachable without going through the registry, so the
|
|
78
|
+
// closed contract has to be enforced on this path too or it is not
|
|
79
|
+
// closed at all.
|
|
80
|
+
const parsed = inputSchema.safeParse(input)
|
|
81
|
+
if (!parsed.success) {
|
|
56
82
|
return {
|
|
57
|
-
success:
|
|
58
|
-
output:
|
|
59
|
-
|
|
83
|
+
success: false,
|
|
84
|
+
output: '',
|
|
85
|
+
error: `Invalid write input: ${parsed.error.issues.map((issue) => issue.message).join('; ')}`,
|
|
60
86
|
}
|
|
61
87
|
}
|
|
88
|
+
const valid = parsed.data
|
|
89
|
+
const content = valid.content ?? valid.newStr ?? ''
|
|
90
|
+
const filePath = resolve(context.workingDirectory, valid.path)
|
|
91
|
+
// The exists-check and the write are a check-then-act pair. Unlocked,
|
|
92
|
+
// two writers both see "absent", both skip the read-before-overwrite
|
|
93
|
+
// guard, and the second silently discards the first.
|
|
94
|
+
const lockKey = `${context.sandbox ? 'sandbox' : 'local'}:${filePath}`
|
|
62
95
|
|
|
63
|
-
|
|
96
|
+
return withFileMutationLock(lockKey, async () => {
|
|
97
|
+
if (context.sandbox) {
|
|
98
|
+
const sandboxExists = await sandboxFileExists(context, valid.path)
|
|
99
|
+
if (sandboxExists) {
|
|
100
|
+
const guard = enforceReadBeforeOverwrite(context, valid.path)
|
|
101
|
+
if (guard) return guard
|
|
102
|
+
}
|
|
103
|
+
await context.sandbox.writeFile(valid.path, content)
|
|
104
|
+
context.fileReadTracker?.recordRead(valid.path)
|
|
105
|
+
return {
|
|
106
|
+
success: true as const,
|
|
107
|
+
output: `File written successfully: ${valid.path} (${content.length} chars) [sandboxed]`,
|
|
108
|
+
data: { path: valid.path, size: content.length, sandboxed: true },
|
|
109
|
+
}
|
|
110
|
+
}
|
|
64
111
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
112
|
+
const localExists = await pathExists(filePath)
|
|
113
|
+
if (localExists) {
|
|
114
|
+
const guard = enforceReadBeforeOverwrite(context, filePath)
|
|
115
|
+
if (guard) return guard
|
|
116
|
+
}
|
|
70
117
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
118
|
+
await mkdir(dirname(filePath), { recursive: true })
|
|
119
|
+
// Temp file, fsync, rename. A plain write that fails partway
|
|
120
|
+
// leaves the destination truncated — and this tool overwrites a
|
|
121
|
+
// whole file, so the truncation is the user's previous work.
|
|
122
|
+
await atomicWriteFile(filePath, content)
|
|
123
|
+
context.fileReadTracker?.recordRead(filePath)
|
|
74
124
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
125
|
+
return {
|
|
126
|
+
success: true as const,
|
|
127
|
+
output: `File written successfully: ${filePath} (${content.length} chars)`,
|
|
128
|
+
data: { path: filePath, size: content.length },
|
|
129
|
+
}
|
|
130
|
+
})
|
|
80
131
|
},
|
|
81
132
|
})
|
|
82
133
|
|
|
@@ -368,3 +368,71 @@ describe('coordinator ask_user_question decision -> output mapping', () => {
|
|
|
368
368
|
expect(result.output).toBe('User answered "Who is the audience?": "Board"')
|
|
369
369
|
})
|
|
370
370
|
})
|
|
371
|
+
|
|
372
|
+
describe('the question contract is closed, not merely shaped', () => {
|
|
373
|
+
const noopHandler: ResumeHandler = async () => ({ action: 'continue' })
|
|
374
|
+
|
|
375
|
+
it('publishes one closed model-facing schema', () => {
|
|
376
|
+
const tool = askTool(noopHandler)
|
|
377
|
+
|
|
378
|
+
expect(tool.modelInputSchema).toMatchObject({
|
|
379
|
+
type: 'object',
|
|
380
|
+
required: ['question', 'options'],
|
|
381
|
+
additionalProperties: false,
|
|
382
|
+
})
|
|
383
|
+
// The whole point of the array declaration: a model that serializes
|
|
384
|
+
// its options once tends to keep doing it, and a closed schema makes
|
|
385
|
+
// a capable provider refuse at generation time instead of after.
|
|
386
|
+
const properties = (tool.modelInputSchema as { properties: Record<string, { type: string }> })
|
|
387
|
+
.properties
|
|
388
|
+
expect(properties.options?.type).toBe('array')
|
|
389
|
+
expect(tool.enforceModelInput).toBe(true)
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it('rejects a serialized options string and other malformed option shapes', () => {
|
|
393
|
+
const tool = askTool(noopHandler)
|
|
394
|
+
|
|
395
|
+
for (const options of [
|
|
396
|
+
'<options><option><label>Board</label></option></options>',
|
|
397
|
+
'[{"label":"Board"},{"label":"Engineering"}]',
|
|
398
|
+
[42, { label: 'Engineering' }],
|
|
399
|
+
[{ description: 'Missing label' }, { label: 'Engineering' }],
|
|
400
|
+
[{ label: 'Board', description: 42 }, { label: 'Engineering' }],
|
|
401
|
+
]) {
|
|
402
|
+
expect(
|
|
403
|
+
tool.inputSchema.safeParse({ question: 'Who is the audience?', options }).success,
|
|
404
|
+
JSON.stringify(options),
|
|
405
|
+
).toBe(false)
|
|
406
|
+
}
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
it('rejects a field it does not declare, rather than dropping it', () => {
|
|
410
|
+
const tool = askTool(noopHandler)
|
|
411
|
+
const valid = {
|
|
412
|
+
question: 'Who is the audience?',
|
|
413
|
+
options: [{ label: 'Board' }, { label: 'Engineering' }],
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
expect(tool.inputSchema.safeParse(valid).success).toBe(true)
|
|
417
|
+
|
|
418
|
+
// Without `.strict()` zod strips these, so the call proceeds as if the
|
|
419
|
+
// caller had never written them — a misspelling becomes a silent no-op.
|
|
420
|
+
for (const extra of [
|
|
421
|
+
{ ...valid, multiSelct: true },
|
|
422
|
+
{ ...valid, choices: ['a', 'b'] },
|
|
423
|
+
{
|
|
424
|
+
...valid,
|
|
425
|
+
options: [{ label: 'Board', reason: 'why' }, { label: 'Engineering' }],
|
|
426
|
+
},
|
|
427
|
+
]) {
|
|
428
|
+
expect(tool.inputSchema.safeParse(extra).success, JSON.stringify(extra)).toBe(false)
|
|
429
|
+
}
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
it('carries a recovery hint that names the shape to retry with', () => {
|
|
433
|
+
const tool = askTool(noopHandler)
|
|
434
|
+
|
|
435
|
+
expect(tool.validationErrorHint).toContain('"options" must be a JSON array')
|
|
436
|
+
expect(tool.validationErrorHint).toContain('never a string')
|
|
437
|
+
})
|
|
438
|
+
})
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { resolvePlanDependencies } from '../plan-dependencies.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The model is shown `depends_on` on every plan step and told it means
|
|
7
|
+
* "Step descriptions this depends on". `approve_plan` then passed
|
|
8
|
+
* `dependsOn: []` for every step, so the ordering it declared was discarded
|
|
9
|
+
* at the only place it entered the system.
|
|
10
|
+
*
|
|
11
|
+
* The cost is not scheduling — the dependency gate in `PlanManager` has no
|
|
12
|
+
* callers — it is the approval. `dependsOn` is serialized into the
|
|
13
|
+
* `plan_approval` payload a human reads before saying yes, so the reviewer
|
|
14
|
+
* saw a plan whose steps all looked independent however carefully the model
|
|
15
|
+
* had ordered them.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const id = (index: number) => `step_${index + 1}`
|
|
19
|
+
|
|
20
|
+
describe('what the model described becomes what the plan holds', () => {
|
|
21
|
+
it('resolves a description to the step that carries it', () => {
|
|
22
|
+
const result = resolvePlanDependencies(
|
|
23
|
+
[
|
|
24
|
+
{ description: 'Read the config' },
|
|
25
|
+
{ description: 'Write the report', depends_on: ['Read the config'] },
|
|
26
|
+
],
|
|
27
|
+
id,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
expect(result).toEqual({ ok: true, dependsOn: [[], ['step_1']] })
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('resolves several dependencies on one step', () => {
|
|
34
|
+
const result = resolvePlanDependencies(
|
|
35
|
+
[
|
|
36
|
+
{ description: 'Read the config' },
|
|
37
|
+
{ description: 'Read the code' },
|
|
38
|
+
{ description: 'Write the report', depends_on: ['Read the config', 'Read the code'] },
|
|
39
|
+
],
|
|
40
|
+
id,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
expect(result.ok && result.dependsOn[2]).toEqual(['step_1', 'step_2'])
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('resolves a forward dependency, since order of declaration is not order of execution', () => {
|
|
47
|
+
const result = resolvePlanDependencies(
|
|
48
|
+
[
|
|
49
|
+
{ description: 'Write the report', depends_on: ['Read the config'] },
|
|
50
|
+
{ description: 'Read the config' },
|
|
51
|
+
],
|
|
52
|
+
id,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
expect(result.ok && result.dependsOn[0]).toEqual(['step_2'])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('leaves a step with no dependencies empty', () => {
|
|
59
|
+
const result = resolvePlanDependencies([{ description: 'Read the config' }], id)
|
|
60
|
+
|
|
61
|
+
expect(result).toEqual({ ok: true, dependsOn: [[]] })
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('forgives whitespace and casing a model did not keep identical', () => {
|
|
65
|
+
const result = resolvePlanDependencies(
|
|
66
|
+
[
|
|
67
|
+
{ description: 'Read the config' },
|
|
68
|
+
{ description: 'Write', depends_on: ['read the CONFIG '] },
|
|
69
|
+
],
|
|
70
|
+
id,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
// Rejecting this would teach the model nothing, for a plan that was
|
|
74
|
+
// right.
|
|
75
|
+
expect(result.ok && result.dependsOn[1]).toEqual(['step_1'])
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('collapses the same dependency named twice', () => {
|
|
79
|
+
const result = resolvePlanDependencies(
|
|
80
|
+
[{ description: 'Read' }, { description: 'Write', depends_on: ['Read', 'Read'] }],
|
|
81
|
+
id,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
expect(result.ok && result.dependsOn[1]).toEqual(['step_1'])
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('a dependency that cannot mean anything is refused, not dropped', () => {
|
|
89
|
+
it('refuses a dependency naming no step', () => {
|
|
90
|
+
const result = resolvePlanDependencies(
|
|
91
|
+
[{ description: 'Write', depends_on: ['Read the config'] }],
|
|
92
|
+
id,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
expect(result.ok).toBe(false)
|
|
96
|
+
// The model has to be able to act on it, so the offending text is named.
|
|
97
|
+
expect(!result.ok && result.error).toContain('Read the config')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('refuses a dependency two steps could answer', () => {
|
|
101
|
+
const result = resolvePlanDependencies(
|
|
102
|
+
[
|
|
103
|
+
{ description: 'Review' },
|
|
104
|
+
{ description: 'Review' },
|
|
105
|
+
{ description: 'Report', depends_on: ['Review'] },
|
|
106
|
+
],
|
|
107
|
+
id,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
// Picking either is a coin flip whose result a human then approves as
|
|
111
|
+
// if it were the model's intent.
|
|
112
|
+
expect(result.ok).toBe(false)
|
|
113
|
+
expect(!result.ok && result.error).toContain('2 steps share that description')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('refuses a step that depends on itself', () => {
|
|
117
|
+
const result = resolvePlanDependencies([{ description: 'Loop', depends_on: ['Loop'] }], id)
|
|
118
|
+
|
|
119
|
+
expect(result.ok).toBe(false)
|
|
120
|
+
expect(!result.ok && result.error).toContain('depends on itself')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('refuses a two-step cycle', () => {
|
|
124
|
+
const result = resolvePlanDependencies(
|
|
125
|
+
[
|
|
126
|
+
{ description: 'A', depends_on: ['B'] },
|
|
127
|
+
{ description: 'B', depends_on: ['A'] },
|
|
128
|
+
],
|
|
129
|
+
id,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
expect(result.ok).toBe(false)
|
|
133
|
+
expect(!result.ok && result.error).toContain('loop')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('refuses a cycle several steps long', () => {
|
|
137
|
+
const result = resolvePlanDependencies(
|
|
138
|
+
[
|
|
139
|
+
{ description: 'A', depends_on: ['C'] },
|
|
140
|
+
{ description: 'B', depends_on: ['A'] },
|
|
141
|
+
{ description: 'C', depends_on: ['B'] },
|
|
142
|
+
],
|
|
143
|
+
id,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
// This is the failure worth catching hardest: no step in a loop can
|
|
147
|
+
// start, so the plan does not error — it simply stops.
|
|
148
|
+
expect(result.ok).toBe(false)
|
|
149
|
+
expect(!result.ok && result.error).toContain('loop')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('names every step in the loop so it can be broken', () => {
|
|
153
|
+
const result = resolvePlanDependencies(
|
|
154
|
+
[
|
|
155
|
+
{ description: 'Alpha', depends_on: ['Gamma'] },
|
|
156
|
+
{ description: 'Beta' },
|
|
157
|
+
{ description: 'Gamma', depends_on: ['Alpha'] },
|
|
158
|
+
],
|
|
159
|
+
id,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
expect(result.ok).toBe(false)
|
|
163
|
+
if (result.ok) return
|
|
164
|
+
expect(result.error).toContain('Alpha')
|
|
165
|
+
expect(result.error).toContain('Gamma')
|
|
166
|
+
// Beta is not in the loop and must not be blamed for it.
|
|
167
|
+
expect(result.error).not.toContain('Beta')
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('accepts a diamond, which is not a cycle', () => {
|
|
171
|
+
const result = resolvePlanDependencies(
|
|
172
|
+
[
|
|
173
|
+
{ description: 'Start' },
|
|
174
|
+
{ description: 'Left', depends_on: ['Start'] },
|
|
175
|
+
{ description: 'Right', depends_on: ['Start'] },
|
|
176
|
+
{ description: 'Join', depends_on: ['Left', 'Right'] },
|
|
177
|
+
],
|
|
178
|
+
id,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
// Two paths reaching one step is an ordinary plan shape, and a naive
|
|
182
|
+
// visited-set cycle check calls it a loop.
|
|
183
|
+
expect(result.ok).toBe(true)
|
|
184
|
+
expect(result.ok && result.dependsOn[3]).toEqual(['step_2', 'step_3'])
|
|
185
|
+
})
|
|
186
|
+
})
|
|
@@ -8,6 +8,7 @@ import type { RunId, TaskId } from '../../types/ids/index.js'
|
|
|
8
8
|
import type { TaskStore } from '../../types/task/index.js'
|
|
9
9
|
import type { ToolDefinition } from '../../types/tool/index.js'
|
|
10
10
|
import { defineTool } from '../defineTool.js'
|
|
11
|
+
import { resolvePlanDependencies } from './plan-dependencies.js'
|
|
11
12
|
|
|
12
13
|
export type TaskLaunchedCallback = (
|
|
13
14
|
agentTaskId: TaskId,
|
|
@@ -78,6 +79,59 @@ const approvePlanStepSchema = z.object({
|
|
|
78
79
|
depends_on: z.array(z.string()).optional().describe('Step descriptions this depends on'),
|
|
79
80
|
})
|
|
80
81
|
|
|
82
|
+
/**
|
|
83
|
+
* The single closed shape a capable provider constrains this call to.
|
|
84
|
+
*
|
|
85
|
+
* `options` arriving as a STRING is the failure this exists for: a model that
|
|
86
|
+
* serializes the array once tends to keep doing it, and the parse error it
|
|
87
|
+
* gets back never says the array was the problem.
|
|
88
|
+
* `additionalProperties: false` turns that into a refusal at generation time
|
|
89
|
+
* rather than a rejection after the fact.
|
|
90
|
+
*/
|
|
91
|
+
const askUserQuestionModelInputSchema: Record<string, unknown> = {
|
|
92
|
+
type: 'object',
|
|
93
|
+
properties: {
|
|
94
|
+
question: {
|
|
95
|
+
type: 'string',
|
|
96
|
+
description: 'Full question text — clear, specific, and ending with a question mark.',
|
|
97
|
+
},
|
|
98
|
+
header: {
|
|
99
|
+
type: 'string',
|
|
100
|
+
description: 'Optional very short topic label, no more than 24 characters.',
|
|
101
|
+
},
|
|
102
|
+
options: {
|
|
103
|
+
type: 'array',
|
|
104
|
+
description: 'A JSON array of 2-4 genuinely distinct, context-derived option objects.',
|
|
105
|
+
items: {
|
|
106
|
+
type: 'object',
|
|
107
|
+
properties: {
|
|
108
|
+
label: {
|
|
109
|
+
type: 'string',
|
|
110
|
+
description:
|
|
111
|
+
'Concise option label. Put the recommended option first and append " (Recommended)".',
|
|
112
|
+
},
|
|
113
|
+
description: {
|
|
114
|
+
type: 'string',
|
|
115
|
+
description: 'Optional one-line explanation of what changes if selected.',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
required: ['label'],
|
|
119
|
+
additionalProperties: false,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
multiSelect: {
|
|
123
|
+
type: 'boolean',
|
|
124
|
+
description: 'True only when several options can apply at once.',
|
|
125
|
+
},
|
|
126
|
+
allowFreeText: {
|
|
127
|
+
type: 'boolean',
|
|
128
|
+
description: 'Whether the user may answer in their own words.',
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
required: ['question', 'options'],
|
|
132
|
+
additionalProperties: false,
|
|
133
|
+
}
|
|
134
|
+
|
|
81
135
|
function normalizeApprovePlanSteps(value: unknown): unknown {
|
|
82
136
|
if (typeof value !== 'string') return value
|
|
83
137
|
|
|
@@ -384,6 +438,15 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
384
438
|
}
|
|
385
439
|
}
|
|
386
440
|
|
|
441
|
+
// Resolve BEFORE touching the plan manager. A refusal here has to
|
|
442
|
+
// leave no half-built plan behind: `startGenerating` replaces the
|
|
443
|
+
// current plan, so failing after it would discard a plan that was
|
|
444
|
+
// fine in favour of one that never completes.
|
|
445
|
+
const dependencies = resolvePlanDependencies(steps, (index) => `step_${index + 1}`)
|
|
446
|
+
if (!dependencies.ok) {
|
|
447
|
+
return { success: false, output: '', error: dependencies.error }
|
|
448
|
+
}
|
|
449
|
+
|
|
387
450
|
pm.startGenerating(title)
|
|
388
451
|
for (let i = 0; i < steps.length; i++) {
|
|
389
452
|
const step = steps[i]
|
|
@@ -392,7 +455,10 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
392
455
|
id: `step_${i + 1}`,
|
|
393
456
|
description: step.description,
|
|
394
457
|
toolName: step.agent_id ? 'create_task' : undefined,
|
|
395
|
-
|
|
458
|
+
// Was `[]` unconditionally, which dropped every ordering
|
|
459
|
+
// constraint the model was invited to express — and put an
|
|
460
|
+
// empty dependency list in front of the human approving it.
|
|
461
|
+
dependsOn: [...(dependencies.dependsOn[i] ?? [])],
|
|
396
462
|
order: i + 1,
|
|
397
463
|
})
|
|
398
464
|
}
|
|
@@ -437,47 +503,55 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
437
503
|
name: 'ask_user_question',
|
|
438
504
|
description:
|
|
439
505
|
'Ask the user ONE question ONLY when you are blocked on a decision that is genuinely theirs to make — one you cannot resolve from their request, your tools, the files you can read, or sensible defaults. The question must be the genuinely undecidable thing in THIS task. Never ask for information a tool can discover (do not ask what you can read, list, or search), never re-ask what the conversation already answers, and never ask meta-questions like "Shall I proceed?" — plan ratification goes through approve_plan. Provide 2-4 genuinely distinct options derived from the actual context — concrete paths, never generic placeholders (for example, asked to prepare a presentation, ask "Who is the audience?" with options like Board / Engineering team / Customer); keep labels short (1-5 words) and give each option a one-line description of what practically changes if it is chosen. Put your recommended option FIRST and append " (Recommended)" to its label. Set multiSelect: true only when several options can apply at once. A free-text "Something else" escape hatch is always shown automatically — do not add your own "Other" option. Ask ONE question per call and prefer at most one question per assistant turn; if several decisions block you, ask only the ones that materially change your next actions, in sequence — most work needs at most 2-3 questions, so prefer proceeding on stated defaults over interrogating the user. Never invent answers or synthetic content on the user\'s behalf unless they explicitly asked for a random/test scenario. The answer arrives as this tool\'s result; if the result says the user did not answer, do not ask this or any other question again — proceed on your best judgment without assuming consent.',
|
|
440
|
-
inputSchema: z
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
.
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
506
|
+
inputSchema: z
|
|
507
|
+
.object({
|
|
508
|
+
question: z
|
|
509
|
+
.string()
|
|
510
|
+
.min(1)
|
|
511
|
+
.describe('Full question text — clear, specific, ends with a question mark.'),
|
|
512
|
+
header: z
|
|
513
|
+
.string()
|
|
514
|
+
.max(24)
|
|
515
|
+
.optional()
|
|
516
|
+
.describe('Very short topic label for the question (e.g. "Audience", "Auth method").'),
|
|
517
|
+
options: z
|
|
518
|
+
.array(
|
|
519
|
+
z
|
|
520
|
+
.object({
|
|
521
|
+
label: z
|
|
522
|
+
.string()
|
|
523
|
+
.min(1)
|
|
524
|
+
.max(80)
|
|
525
|
+
.describe(
|
|
526
|
+
'Concise option label (1-5 words). Recommended option goes first with " (Recommended)" appended.',
|
|
527
|
+
),
|
|
528
|
+
description: z
|
|
529
|
+
.string()
|
|
530
|
+
.max(300)
|
|
531
|
+
.optional()
|
|
532
|
+
.describe('One line on what practically changes if this option is chosen.'),
|
|
533
|
+
})
|
|
534
|
+
.strict(),
|
|
535
|
+
)
|
|
536
|
+
.min(2)
|
|
537
|
+
.max(4)
|
|
538
|
+
.describe('2-4 genuinely distinct, context-derived options.'),
|
|
539
|
+
multiSelect: z
|
|
540
|
+
.boolean()
|
|
541
|
+
.optional()
|
|
542
|
+
.default(false)
|
|
543
|
+
.describe('True only when several options can apply at once.'),
|
|
544
|
+
allowFreeText: z
|
|
545
|
+
.boolean()
|
|
546
|
+
.optional()
|
|
547
|
+
.default(true)
|
|
548
|
+
.describe('Whether the user may answer in their own words.'),
|
|
549
|
+
})
|
|
550
|
+
.strict(),
|
|
551
|
+
modelInputSchema: structuredClone(askUserQuestionModelInputSchema),
|
|
552
|
+
enforceModelInput: true,
|
|
553
|
+
validationErrorHint:
|
|
554
|
+
'Required shape: {"question":"...?","options":[{"label":"First (Recommended)","description":"What changes"},{"label":"Second","description":"What changes"}]}. "options" must be a JSON array of 2-4 objects, never a string.',
|
|
481
555
|
category: 'custom',
|
|
482
556
|
permissions: [],
|
|
483
557
|
readOnly: true,
|