fix: preserve multiple positional invocation arguments and update API schema

This commit is contained in:
2026-10-05 19:07:23 +09:00
parent f7783b3d2a
commit 31f7847d1f
3 changed files with 498 additions and 6 deletions
+475 -5
View File
@@ -1075,6 +1075,70 @@
], ],
"type": "object" "type": "object"
}, },
"CompletionContext": {
"properties": {
"argument": {
"type": [
"string",
"null"
]
},
"invocation": {
"$ref": "#/components/schemas/FeatureInvocationIdentity"
}
},
"required": [
"invocation"
],
"type": "object"
},
"CompletionEntry": {
"description": "One completion candidate for a prefix query.",
"properties": {
"description": {
"type": [
"string",
"null"
]
},
"invocation": {
"anyOf": [
{
"$ref": "#/components/schemas/FeatureInvocationDescriptor"
},
{
"type": "null"
}
]
},
"is_dir": {
"default": false,
"type": "boolean"
},
"usage": {
"type": [
"string",
"null"
]
},
"value": {
"type": "string"
}
},
"required": [
"value"
],
"type": "object"
},
"CompletionKind": {
"description": "Kind of completion requested by `Method::ListCompletions`.\n\n`File` is the existing `@` path lane. `Feature` lists installed chat\ninvocations. `FeatureArgument` resolves a declared argument using the\nsource-qualified [`CompletionContext`].",
"enum": [
"file",
"feature",
"feature_argument"
],
"type": "string"
},
"ConfigCommitRequest": { "ConfigCommitRequest": {
"additionalProperties": false, "additionalProperties": false,
"description": "Compare-and-swap request for applying a complete set of source-tree changes.", "description": "Compare-and-swap request for applying a complete set of source-tree changes.",
@@ -2232,6 +2296,98 @@
}, },
"type": "object" "type": "object"
}, },
"FeatureInvocation": {
"properties": {
"arguments": {
"items": {
"$ref": "#/components/schemas/InvocationArgumentValue"
},
"type": "array"
},
"identity": {
"$ref": "#/components/schemas/FeatureInvocationIdentity"
},
"invocation_id": {
"description": "Stable client-generated id retained across queueing and transport retry.",
"type": "string"
},
"name": {
"description": "Selected public name, retained for exact draft/history restoration. Host\nresolution is always by `identity`.",
"type": "string"
}
},
"required": [
"invocation_id",
"identity",
"name",
"arguments"
],
"type": "object"
},
"FeatureInvocationDescriptor": {
"properties": {
"aliases": {
"default": [],
"items": {
"type": "string"
},
"type": "array"
},
"arguments": {
"default": [],
"items": {
"$ref": "#/components/schemas/InvocationArgumentDescriptor"
},
"type": "array"
},
"client_adapter": {
"anyOf": [
{
"$ref": "#/components/schemas/InvocationClientAdapter"
},
{
"type": "null"
}
]
},
"description": {
"type": "string"
},
"display_name": {
"type": "string"
},
"identity": {
"$ref": "#/components/schemas/FeatureInvocationIdentity"
},
"name": {
"type": "string"
},
"syntax": {
"$ref": "#/components/schemas/FeatureInvocationSyntax"
}
},
"required": [
"identity",
"name",
"display_name",
"description",
"syntax"
],
"type": "object"
},
"FeatureInvocationIdentity": {
"description": "Source-qualified identity of one invocation contribution, for example\n`builtin:attachments/attach`. Display names and aliases are never authority.",
"type": "string"
},
"FeatureInvocationSyntax": {
"oneOf": [
{
"const": "parenthesized",
"description": "`/name(value, key=value)` with JSON-style quoted strings and `)` as the\nexplicit boundary before following natural language.",
"type": "string"
}
]
},
"FindingSeverity": { "FindingSeverity": {
"enum": [ "enum": [
"blocker", "blocker",
@@ -2609,6 +2765,285 @@
], ],
"type": "object" "type": "object"
}, },
"InvocationArgumentDescriptor": {
"properties": {
"completion": {
"$ref": "#/components/schemas/InvocationCompletion",
"default": {
"kind": "none"
}
},
"description": {
"type": [
"string",
"null"
]
},
"name": {
"type": "string"
},
"position": {
"description": "Zero-based positional index. `None` means named-only. A positional\nargument may also be supplied by name; this makes chip editing stable.",
"format": "uint16",
"maximum": 65535,
"minimum": 0,
"type": [
"integer",
"null"
]
},
"required": {
"default": false,
"type": "boolean"
},
"value_type": {
"$ref": "#/components/schemas/InvocationArgumentType"
}
},
"required": [
"name",
"value_type"
],
"type": "object"
},
"InvocationArgumentType": {
"oneOf": [
{
"properties": {
"kind": {
"const": "string",
"type": "string"
}
},
"required": [
"kind"
],
"type": "object"
},
{
"properties": {
"kind": {
"const": "integer",
"type": "string"
}
},
"required": [
"kind"
],
"type": "object"
},
{
"properties": {
"kind": {
"const": "boolean",
"type": "string"
}
},
"required": [
"kind"
],
"type": "object"
},
{
"properties": {
"kind": {
"const": "enum",
"type": "string"
},
"values": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"kind",
"values"
],
"type": "object"
},
{
"description": "A Worker-readable path. This does not grant filesystem authority.",
"properties": {
"kind": {
"const": "worker_file",
"type": "string"
}
},
"required": [
"kind"
],
"type": "object"
},
{
"description": "A resource selected and staged by the client. It must never be resolved\nas a path on the Worker host.",
"properties": {
"kind": {
"const": "client_file",
"type": "string"
}
},
"required": [
"kind"
],
"type": "object"
}
]
},
"InvocationArgumentValue": {
"properties": {
"name": {
"type": "string"
},
"value": {
"$ref": "#/components/schemas/InvocationValue"
}
},
"required": [
"name",
"value"
],
"type": "object"
},
"InvocationClientAdapter": {
"oneOf": [
{
"const": "attachment",
"description": "The client stages a local file and replaces the draft invocation with an\n`UploadedFile` chip. Local paths are not sent to or persisted by Worker.",
"type": "string"
}
]
},
"InvocationCompletion": {
"oneOf": [
{
"properties": {
"kind": {
"const": "none",
"type": "string"
}
},
"required": [
"kind"
],
"type": "object"
},
{
"properties": {
"kind": {
"const": "static",
"type": "string"
},
"values": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"kind",
"values"
],
"type": "object"
},
{
"properties": {
"kind": {
"const": "worker_file",
"type": "string"
}
},
"required": [
"kind"
],
"type": "object"
},
{
"properties": {
"kind": {
"const": "client_file",
"type": "string"
}
},
"required": [
"kind"
],
"type": "object"
},
{
"description": "Host-owned provider selected by source-qualified id. Metadata lookup is\nside-effect free; implementations never cross this wire boundary.",
"properties": {
"kind": {
"const": "provider",
"type": "string"
},
"provider": {
"type": "string"
}
},
"required": [
"kind",
"provider"
],
"type": "object"
}
]
},
"InvocationValue": {
"oneOf": [
{
"properties": {
"kind": {
"const": "string",
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"kind",
"value"
],
"type": "object"
},
{
"properties": {
"kind": {
"const": "integer",
"type": "string"
},
"value": {
"format": "int32",
"type": "integer"
}
},
"required": [
"kind",
"value"
],
"type": "object"
},
{
"properties": {
"kind": {
"const": "boolean",
"type": "string"
},
"value": {
"type": "boolean"
}
},
"required": [
"kind",
"value"
],
"type": "object"
}
]
},
"LegacyMemoryResetRequest": { "LegacyMemoryResetRequest": {
"additionalProperties": false, "additionalProperties": false,
"description": "Explicit destructive reset request. The confirmation must exactly equal\n`RESET LEGACY MEMORY <workspace_id>` for the Workspace named in the route.", "description": "Explicit destructive reset request. The confirmation must exactly equal\n`RESET LEGACY MEMORY <workspace_id>` for the Workspace named in the route.",
@@ -7356,7 +7791,19 @@
"RuntimeWorkerCompletionsRequest": { "RuntimeWorkerCompletionsRequest": {
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"kind": true, "context": {
"anyOf": [
{
"$ref": "#/components/schemas/CompletionContext"
},
{
"type": "null"
}
]
},
"kind": {
"$ref": "#/components/schemas/CompletionKind"
},
"prefix": { "prefix": {
"default": "", "default": "",
"type": "string" "type": "string"
@@ -7378,10 +7825,14 @@
"type": "array" "type": "array"
}, },
"entries": { "entries": {
"items": true, "items": {
"$ref": "#/components/schemas/CompletionEntry"
},
"type": "array" "type": "array"
}, },
"kind": true, "kind": {
"$ref": "#/components/schemas/CompletionKind"
},
"prefix": { "prefix": {
"type": "string" "type": "string"
}, },
@@ -7415,7 +7866,9 @@
] ]
}, },
"segments": { "segments": {
"items": true, "items": {
"$ref": "#/components/schemas/Segment"
},
"type": [ "type": [
"array", "array",
"null" "null"
@@ -8262,6 +8715,23 @@
], ],
"type": "object" "type": "object"
}, },
{
"description": "Explicitly selected, source-qualified Feature invocation. The host never\npromotes free text to this variant and validates it against the installed\ndeclaration before execution.",
"properties": {
"invocation": {
"$ref": "#/components/schemas/FeatureInvocation"
},
"kind": {
"const": "feature_invoke",
"type": "string"
}
},
"required": [
"kind",
"invocation"
],
"type": "object"
},
{ {
"description": "Source-qualified request for the host to start a Flow before committing\nthis user input. Runtime resolves it through Workspace authority and\nreplaces it with the entered state's instructions before Worker-side\ninput resolution.", "description": "Source-qualified request for the host to start a Flow before committing\nthis user input. Runtime resolves it through Workspace authority and\nreplaces it with the entered state's instructions before Worker-side\ninput resolution.",
"properties": { "properties": {
@@ -14794,7 +15264,7 @@
"info": { "info": {
"title": "Yoi Server API", "title": "Yoi Server API",
"version": "0.1.0", "version": "0.1.0",
"x-yoi-source-digest": "sha256:f9f0f8918dbcce386e94d5368fa938f02565ca98f9f67f07537bd88ed8c49870" "x-yoi-source-digest": "sha256:0dc47aa72f9cace6e0026fbf5d9385f51b6d79c8f6a70a04954067a35e01c887"
}, },
"openapi": "3.1.0", "openapi": "3.1.0",
"paths": { "paths": {
@@ -65,6 +65,27 @@ Deno.test("selected invocation parsing preserves Unicode, spaces, slashes, prose
]); ]);
}); });
Deno.test("multiple positional arguments advance once per value independent of descriptor order", () => {
const positional: FeatureInvocationDescriptor = {
...descriptor,
arguments: [
{ name: "enabled", position: 2, required: true, value_type: { kind: "boolean" }, completion: { kind: "none" } },
{ name: "mode", position: 0, required: true, value_type: { kind: "enum", values: ["brief", "full"] }, completion: { kind: "none" } },
{ name: "limit", position: 1, required: true, value_type: { kind: "integer" }, completion: { kind: "none" } },
{ name: "note", required: false, value_type: { kind: "string" }, completion: { kind: "none" } },
],
};
for (const input of ['/run("brief", 2, true) following prose', '/run("brief", note="資料/a b", 2, true) following prose']) {
const parsed = selectedFeatureInvocationRanges(input, [positional])[0];
assertEquals(input.slice(parsed.end), " following prose");
assertEquals(parsed.invocation.arguments.filter((argument) => argument.name !== "note"), [
{ name: "mode", value: { kind: "string", value: "brief" } },
{ name: "limit", value: { kind: "integer", value: 2 } },
{ name: "enabled", value: { kind: "boolean", value: true } },
]);
}
});
Deno.test("unselected text, URLs, paths, and paste content are never promoted", () => { Deno.test("unselected text, URLs, paths, and paste content are never promoted", () => {
assertEquals( assertEquals(
finalizeSelectedFeatureInvocations( finalizeSelectedFeatureInvocations(
@@ -278,7 +278,8 @@ function parseFeatureInvocation(
while (/\s/.test(text[cursor] ?? "")) cursor++; while (/\s/.test(text[cursor] ?? "")) cursor++;
} else { } else {
cursor = itemStart; cursor = itemStart;
argument = descriptor.arguments.find((candidate) => candidate.position === position++); argument = descriptor.arguments.find((candidate) => candidate.position === position);
position++;
if (!argument) throw new Error("Too many positional Feature arguments."); if (!argument) throw new Error("Too many positional Feature arguments.");
} }
const parsed = parseRawValue(text, cursor); const parsed = parseRawValue(text, cursor);