feat: add declarative Feature chat invocations and typed composers

This commit is contained in:
2026-10-05 18:44:41 +09:00
parent cc97987482
commit f7783b3d2a
69 changed files with 10065 additions and 657 deletions
+45 -4
View File
@@ -8233,7 +8233,7 @@ pub struct RuntimeWorkerInputRequest {
pub kind: Option<String>,
pub content: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub segments: Option<Vec<serde_json::Value>>,
pub segments: Option<Vec<protocol::Segment>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@@ -8268,9 +8268,11 @@ pub struct RuntimeWorkerLifecycleResult {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct RuntimeWorkerCompletionsRequest {
pub kind: serde_json::Value,
pub kind: protocol::CompletionKind,
#[serde(default)]
pub prefix: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context: Option<protocol::CompletionContext>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
@@ -8278,9 +8280,9 @@ pub struct RuntimeWorkerCompletionsRequest {
pub struct RuntimeWorkerCompletionsResult {
pub runtime_id: String,
pub worker_id: String,
pub kind: serde_json::Value,
pub kind: protocol::CompletionKind,
pub prefix: String,
pub entries: Vec<serde_json::Value>,
pub entries: Vec<protocol::CompletionEntry>,
#[serde(default)]
pub diagnostics: Vec<Diagnostic>,
}
@@ -12661,6 +12663,45 @@ mod tests {
assert!(serde_json::from_value::<CompanionTranscriptItem>(private_item).is_err());
}
#[test]
fn structured_invocation_input_round_trips_through_typed_api() {
let json = serde_json::json!({"kind":"user", "content":"", "segments":[{"kind":"feature_invoke", "invocation":{
"invocation_id":"stable-invoke", "identity":"builtin:test/prepare", "name":"prepare",
"arguments":[{"name":"path", "value":{"kind":"string","value":"資料/a b"}}]
}}]});
let request: RuntimeWorkerInputRequest = serde_json::from_value(json.clone()).unwrap();
assert!(
matches!(&request.segments.as_ref().unwrap()[0], protocol::Segment::FeatureInvoke { invocation }
if invocation.identity.0 == "builtin:test/prepare" && invocation.invocation_id == "stable-invoke")
);
assert_eq!(serde_json::to_value(request).unwrap(), json);
let mut invalid = json;
invalid["segments"][0]["invocation"]["arguments"][0]["value"] =
serde_json::json!({"kind":"integer","value":2147483648_i64});
assert!(serde_json::from_value::<RuntimeWorkerInputRequest>(invalid).is_err());
}
#[test]
fn feature_completion_contract_is_typed_and_preserves_argument_context() {
let json = serde_json::json!({"kind":"feature_argument", "prefix":"資料/", "context":{"invocation":"builtin:test/prepare","argument":"path"}});
let request: RuntimeWorkerCompletionsRequest =
serde_json::from_value(json.clone()).unwrap();
assert_eq!(request.kind, protocol::CompletionKind::FeatureArgument);
assert_eq!(serde_json::to_value(request).unwrap(), json);
assert!(
serde_json::from_value::<RuntimeWorkerCompletionsRequest>(
serde_json::json!({"kind":"unknown"})
)
.is_err()
);
assert!(
serde_json::from_value::<RuntimeWorkerCompletionsRequest>(
serde_json::json!({"kind":"feature_argument", "context":{"invocation":123}})
)
.is_err()
);
}
#[test]
fn generated_companion_api_contract_is_current() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
@@ -1227,8 +1227,16 @@ fn json_content_schema<'a>(
}
fn schema_reference<'a>(value: &'a Value, context: &str) -> Result<&'a str, GenerationError> {
const ALLOWED_REFERENCE_SIBLINGS: &[&str] =
&["$ref", "$comment", "deprecated", "description", "title"];
// `default` is a JSON Schema annotation, not a constraint or a replacement
// for the referenced type. Schemars emits it for defaulted enum fields.
const ALLOWED_REFERENCE_SIBLINGS: &[&str] = &[
"$ref",
"$comment",
"default",
"deprecated",
"description",
"title",
];
let schema = object(value, context)?;
if let Some(keyword) = schema
.keys()
@@ -2023,6 +2031,27 @@ mod tests {
assert!(error.to_string().contains("ambiguous anyOf"));
}
#[test]
fn reference_default_annotations_do_not_replace_the_referenced_type() {
let value = serde_json::json!({"$ref":"#/components/schemas/InvocationCompletion", "default":{"kind":"none"}});
assert_eq!(
schema_reference(&value, "completion").unwrap(),
"#/components/schemas/InvocationCompletion"
);
let output = generate_runtime_typescript(
&crate::canonical_openapi_document()
.unwrap()
.to_json()
.unwrap(),
)
.unwrap();
assert!(output.contains("completion?: InvocationCompletion"));
assert!(output.contains("export type FeatureInvocation ="));
assert!(output.contains(
"export type CompletionKind = \"file\" | \"feature\" | \"feature_argument\""
));
}
#[test]
fn generator_rejects_lossy_reference_siblings() {
let mut document: Value = serde_json::from_str(OPENAPI).unwrap();