polyc-agent 2026.9.0

The agent turn loop: provider + tool-call routing, shared by the control plane and harness.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
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
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
//! Delegation (in-process sub-agent task) primitive for the agent turn loop.
//!
//! # Why a reserved tool name, joining the batch (not short-circuiting it)
//!
//! This mirrors [`crate::handoff`]'s reserved-tool-name design (a model-facing
//! function the loop recognizes by name, so no new provider integration or
//! wire channel is needed) but the two primitives have OPPOSITE control flow:
//!
//!   * A **handoff** suspends the whole turn — the parent conversation stops
//!     and a child `Conversation` is created. The transfer is one-way: the
//!     child's result never returns to the parent. It short-circuits the
//!     batch: no other tool in the same batch executes.
//!   * A **delegation** (`__delegate_to`, #870) runs a nested, context-
//!     isolated turn IN-PROCESS, synchronously, as part of dispatching this
//!     SAME batch — it joins `run_turn_with`'s ordinary `tool_futures`
//!     alongside every other call in the batch, and its result is just
//!     another `tool_result` the SAME turn's next provider step sees. There
//!     is no suspend, no child resource, no later turn.
//!
//! This is the tracer bullet for PRD #867: exactly one task, to exactly one
//! worker, capped at one level deep (a worker's own advertised tool set never
//! includes `__delegate_to` — see [`crate::run_turn_with`]'s tool-spec
//! pinning).

use std::sync::Arc;

use polyc_llm::{DynProvider, ToolSpec};

/// Operator-declared ceiling on the parent files a delegated worker may be
/// seeded with (`#2295`).
///
/// `#2286` fenced every worker in its own workspace subtree and re-rooted the
/// coding tools against it, reads included — which ended the previously
/// implicit contract that a worker could read the parent's workspace. This
/// ceiling is the static half of restoring it: the target agent's manifest
/// declares the maximum reachable set, and a `__delegate_to` call names
/// specific paths *within* it (see [`DelegateRequest::share_in`]). The
/// orchestrating model can therefore pick the file a task is about without
/// being able to widen what any worker of that agent can ever see.
///
/// The default is CLOSED: an empty [`Self::allow`] seeds nothing, so an agent
/// whose manifest says nothing about share-in keeps `#2286`'s behavior exactly.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ShareInCeiling {
    /// Glob patterns, matched against parent-workspace-relative file paths.
    /// A file is eligible only if it matches at least one pattern. Empty ⇒
    /// nothing is eligible.
    pub allow: Vec<String>,
    /// Maximum number of files one delegation may seed. Zero ⇒ nothing is
    /// eligible, matching the empty-`allow` closed default.
    pub max_files: usize,
    /// Maximum total bytes one delegation may seed, so a fan-out of workers
    /// cannot exhaust the workspace volume's `sizeLimit`. Zero ⇒ nothing is
    /// eligible.
    pub max_bytes: u64,
}

impl ShareInCeiling {
    /// Whether this ceiling can admit any file at all.
    ///
    /// Every zero/empty field independently closes the ceiling, so a partially
    /// configured manifest fails closed rather than admitting an unbounded set.
    #[must_use]
    pub const fn admits_anything(&self) -> bool {
        !self.allow.is_empty() && self.max_files > 0 && self.max_bytes > 0
    }
}

/// One delegated worker's identity and share-in request, handed to
/// [`crate::ToolExecutor::for_worker`] (`#2295`).
///
/// Carries the call-site request and the descriptor ceiling together so the
/// executor that owns the workspace — the only layer that knows both the
/// parent root and the worker root — can enforce one against the other in a
/// single step, rather than re-rooting first and seeding through a second
/// method a wrapper could forget to forward.
#[derive(Debug, Clone, Copy)]
pub struct WorkerScope<'a> {
    /// The worker's delegate call id, which keys its workspace subtree.
    pub worker_id: &'a str,
    /// Parent-workspace-relative paths the call asked to seed. Each entry is a
    /// literal path — a file, or a directory seeded recursively. Empty ⇒ no
    /// seeding, and the worker starts on an empty scratch space.
    pub share_in: &'a [String],
    /// The target agent's ceiling, which [`Self::share_in`] must fall within.
    pub ceiling: &'a ShareInCeiling,
}

impl<'a> WorkerScope<'a> {
    /// A scope that seeds nothing — `#2286`'s behavior, and what every caller
    /// that has no share-in request to make should pass.
    #[must_use]
    pub const fn bare(worker_id: &'a str) -> Self {
        Self {
            worker_id,
            share_in: &[],
            ceiling: &EMPTY_CEILING,
        }
    }
}

/// Backing storage for [`WorkerScope::bare`]'s ceiling reference.
///
/// A `static` rather than a `const`: [`ShareInCeiling`] owns a [`Vec`], so it
/// carries a `Drop` impl and `&CONST` would not promote to `'static`.
static EMPTY_CEILING: ShareInCeiling = ShareInCeiling {
    allow: Vec::new(),
    max_files: 0,
    max_bytes: 0,
};

/// Why a share-in request was refused (`#2295`).
///
/// Every variant is a hard refusal that fails the delegation: seeding is
/// bounded by an operator ceiling precisely so exceeding it is an error the
/// caller sees, not a silent truncation that hands the worker an arbitrary
/// subset of what the task needed.
#[derive(Debug, thiserror::Error)]
pub enum ShareInError {
    /// The path left the parent workspace, or named an absolute location.
    #[error("cannot share in `{path}`: {reason}")]
    Escapes {
        /// The offending request entry.
        path: String,
        /// [`polyc_tools`-style lexical containment's] own reason string.
        ///
        /// [`polyc_tools`-style lexical containment's]: crate::ToolExecutor::for_worker
        reason: String,
    },
    /// The path reached into a delegated worker's own subtree — a sibling's
    /// scratch space, or this worker's. Seeding from one would hand a worker
    /// exactly the cross-worker read `#2286` fenced off.
    #[error("cannot share in `{path}`: it is inside a delegated worker's workspace")]
    WorkerSubtree {
        /// The offending request entry.
        path: String,
    },
    /// The file exists and is inside the workspace, but no ceiling pattern
    /// admits it.
    #[error("cannot share in `{path}`: this agent's share-in ceiling does not include it")]
    OutsideCeiling {
        /// The offending workspace-relative file path.
        path: String,
    },
    /// The request named more files than the ceiling admits.
    #[error("cannot share in {found} files: this agent's ceiling allows {limit}")]
    TooManyFiles {
        /// How many files the request resolved to.
        found: usize,
        /// [`ShareInCeiling::max_files`].
        limit: usize,
    },
    /// The request named more bytes than the ceiling admits.
    #[error("cannot share in {found} bytes: this agent's ceiling allows {limit}")]
    TooManyBytes {
        /// How many bytes the request resolved to.
        found: u64,
        /// [`ShareInCeiling::max_bytes`].
        limit: u64,
    },
    /// The path named nothing in the parent workspace.
    #[error("cannot share in `{path}`: no such file or directory in the workspace")]
    NotFound {
        /// The offending request entry.
        path: String,
    },
    /// Reading the parent file or writing the worker's copy failed.
    #[error("could not share in `{path}`: {reason}")]
    Io {
        /// The offending workspace-relative path.
        path: String,
        /// The underlying I/O error, rendered.
        reason: String,
    },
}

/// A delegated worker's re-rooted executor plus what was seeded into it
/// (`#2295`).
pub struct WorkerHandoff {
    /// The executor scoped to the worker's own workspace subtree.
    pub tools: Arc<dyn crate::ToolExecutor>,
    /// Workspace-relative paths seeded into that subtree, in the order they
    /// were copied — recorded on the delegation's forensic record so a seed is
    /// attributable to the delegate call id that requested it.
    pub seeded: Vec<String>,
}

impl std::fmt::Debug for WorkerHandoff {
    /// Hand-rolled: [`crate::ToolExecutor`] carries no `Debug` bound, so the
    /// executor is elided and only the seeded set is rendered.
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("WorkerHandoff")
            .field("tools", &"<dyn ToolExecutor>")
            .field("seeded", &self.seeded)
            .finish()
    }
}

/// The reserved tool name the model emits to request an in-process
/// delegation to a scoped worker agent.
///
/// Advertised only when [`crate::RunTurnOptions::delegate_descriptors`] is
/// non-empty (see [`delegate_tool_spec`]'s call site in `run_turn_with`) — a
/// conversation whose agent declares no delegation targets never sees this
/// name at all, so it can't collide with a real tool of the same name either.
pub const DELEGATE_TOOL_NAME: &str = "__delegate_to";

/// The condensation contract appended to every delegated worker's
/// synthesized instructions when the call carries no `result_schema`
/// (INV-C25, #1140).
///
/// The worker's final message is the sole return channel back to the
/// caller, so the worker is told to make that message a self-contained
/// summary of the outcome. When a `result_schema` IS in force, the
/// schema-forced finalize path bounds the answer's shape instead and this
/// text is not injected. The per-call result cap (`MAX_TOOL_RESULT_BYTES`
/// middle-elision) stays as the hard backstop either way — this contract
/// instructs, the cap enforces.
pub const WORKER_CONDENSATION_CONTRACT: &str = "You are completing one delegated task. The \
    caller sees only your final message — none of your tool calls, intermediate work, or \
    earlier drafts reach it. Make your final message a self-contained summary of the outcome: \
    what you did or found, the key details the caller needs, and anything that failed. Keep it \
    concise — an overlong answer is trimmed from the middle.";

/// Composes a delegated worker's synthesized system message text
/// (INV-C25, `#1140`), given the descriptor's own (already trimmed,
/// non-empty-or-`None`) `instructions` and whether the call carries a
/// `result_schema`.
///
/// With no `result_schema` in force, the worker's final message is the sole
/// return channel, so [`WORKER_CONDENSATION_CONTRACT`] is always appended
/// (on its own line pair after `instructions`, or standalone when
/// `instructions` is `None`) — this branch never returns `None`. With a
/// `result_schema` in force, the schema-forced finalize path bounds the
/// answer's shape instead, so the contract text is NOT injected and
/// `instructions` passes through unchanged (`None` stays `None`).
#[must_use]
pub(crate) fn worker_system_text(
    instructions: Option<&str>,
    has_result_schema: bool,
) -> Option<String> {
    if has_result_schema {
        return instructions.map(str::to_owned);
    }
    Some(instructions.map_or_else(
        || WORKER_CONDENSATION_CONTRACT.to_owned(),
        |instructions| format!("{instructions}\n\n{WORKER_CONDENSATION_CONTRACT}"),
    ))
}

/// Renders a delegated worker's turn-start system message (`#1323`),
/// mirroring `polyc_control_plane`'s top-level `turn_start_block` — same
/// wording, same UTC-at-minute-precision rendering — from `unix_ms`, the
/// PARENT turn's frozen dispatch clock
/// (`RunTurnOptions::turn_start_unix_ms`), never an independent read: a
/// worker's nested turn has no dispatch clock of its own to freeze, and
/// reading one here would break replay determinism (INV-11).
///
/// `None` when `unix_ms` falls outside `jiff::Timestamp`'s representable
/// range (in practice, only a caller passing `u64::MAX`) — a worker told
/// nothing is safer than one told a wrong time, the same rule the top-level
/// stamp follows.
///
/// Pushed as its OWN system message (see [`crate::run_delegate_call`]),
/// never folded into [`worker_system_text`]'s returned string: the
/// instructions/condensation text is the worker prompt's stable content,
/// and this value changes on every dispatch, so joining them would defeat
/// any future caching of the stable part.
#[must_use]
pub(crate) fn worker_turn_start_block(unix_ms: u64) -> Option<String> {
    let instant = i64::try_from(unix_ms)
        .ok()
        .and_then(|ms| jiff::Timestamp::from_millisecond(ms).ok())?
        .strftime("%Y-%m-%d %H:%M")
        .to_string();
    Some(format!(
        "This turn started at {instant} UTC. Later steps in this turn may \
         run after this instant."
    ))
}

/// JSON-schema spec for the delegate tool. Provided alongside the user's tool
/// specs, but ONLY when at least one [`DelegateDescriptor`] is configured —
/// see [`delegate_tool_spec`].
#[must_use]
pub fn delegate_tool_spec() -> ToolSpec {
    // Like the handoff primitive, delegation is a runtime mechanism the
    // capability gate never mediates (the orchestrator-level call is always
    // allowed) — the worker's OWN nested turn re-applies the full gate to
    // everything it does, fail-closed (see `run_turn_with`'s unattended-mode
    // wiring for the nested options).
    ToolSpec::new(
        DELEGATE_TOOL_NAME,
        "Hand a single, self-contained task to a specialized worker and wait for its answer. \
         The worker runs in an isolated context — it does NOT see this conversation's history, \
         only `task` and, if given, `context` — so state everything the worker needs to know. \
         `target_agent_id` selects which worker runs the task. Set `result_schema` (a JSON \
         Schema) to force the worker's answer into that shape instead of free text — the worker \
         gets one retry if its first answer doesn't match, and reports a structured failure if it \
         still can't conform.",
        serde_json::json!({
            "type": "object",
            "properties": {
                "target_agent_id": {
                    "type": "string",
                    "description": "Identifier of the worker agent to run the task."
                },
                "task": {
                    "type": "string",
                    "description": "The self-contained task for the worker to perform."
                },
                "context": {
                    "type": "string",
                    "description": "Optional extra context the worker needs — the worker sees no \
                        other history, so include anything relevant here."
                },
                "result_schema": {
                    "type": "object",
                    "description": "Optional JSON Schema the worker's final answer must satisfy. \
                        Omit for a free-text answer."
                },
                "share_in": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Optional workspace files to copy into the worker's own \
                        workspace before it starts. The worker has a separate workspace and \
                        cannot see yours, so name every file its task is about — each entry is \
                        a path relative to the workspace, either a file or a directory. The \
                        worker gets its own copy; its edits never reach your files."
                }
            },
            "required": ["target_agent_id", "task"],
            "additionalProperties": false
        }),
    )
}

/// A resolved, self-contained worker configuration for one `can_delegate_to`
/// target (#870).
///
/// Built by the control plane at turn dispatch — NEVER by this crate — and
/// threaded down through the wire (`TurnInput.delegate_descriptors`) and the
/// harness's tool-executor composition
/// (`polyc_turn_runner::resolve_delegate_descriptors`) into
/// [`crate::RunTurnOptions::delegate_descriptors`]. See
/// `crates/control-plane/src/delegate.rs` for how the fields here are
/// resolved (provider/model fallback, connector-scope intersection, the
/// read-only-by-default built-in allowlist).
#[derive(Clone)]
pub struct DelegateDescriptor {
    /// The target `Agent` resource name — matched (trailing-name, mirroring
    /// [`crate::HandoffRequest::child_agent_id`]'s resolution) against the
    /// model's `__delegate_to(target_agent_id, ...)` argument to pick this
    /// descriptor. See [`find_descriptor`].
    pub agent_id: String,
    /// System instructions for the worker's nested turn. `None` ⇒ no
    /// agent-specific instructions.
    pub instructions: Option<String>,
    /// The worker's resolved backend, already picked from the deployment's
    /// registered providers — this crate never resolves a provider selector
    /// string itself.
    pub provider: Arc<DynProvider>,
    /// The registry key of [`Self::provider`] (e.g. `"vertex"`, `"stub"`) —
    /// carried alongside the erased backend so a forensic record (`#872`,
    /// `DelegateRecord::resolved_provider`) can name the provider without
    /// this crate needing a `Debug`/name accessor on [`DynProvider`] itself.
    pub provider_name: String,
    /// The worker's resolved model id.
    pub model: String,
    /// The worker's advertised tool specs. Never includes
    /// [`DELEGATE_TOOL_NAME`] — this is what caps delegation depth at one,
    /// since [`crate::run_turn_with`] only advertises the delegate tool when
    /// its OWN `delegate_descriptors` option is non-empty, and a nested turn
    /// always runs with that option empty.
    pub tool_specs: Vec<ToolSpec>,
    /// The worker's step budget, applied to the nested turn's
    /// `RunTurnOptions::max_steps`.
    pub max_steps: usize,
    /// Whether the worker's nested turn may ground on the provider's native
    /// web-search primitive (`RunTurnOptions::native_search_allowed`).
    /// Derived the same way the parent turn's own scoping is (`#1226`): the
    /// resolved descriptor's `builtin_tools` named
    /// `polyc_tools::web::NATIVE_SEARCH_GROUNDING` — never hardcoded true,
    /// since that would hand every worker a capability its own agent
    /// manifest never granted.
    pub native_search_allowed: bool,
    /// The ceiling on parent files a worker of this agent may be seeded with
    /// (`#2295`), from the target agent's manifest. Defaults closed, so an
    /// agent that declares no share-in keeps `#2286`'s fully-fenced worker.
    pub share_in: ShareInCeiling,
}

impl std::fmt::Debug for DelegateDescriptor {
    /// Hand-rolled: [`DynProvider`] carries no `Debug` impl (the `LlmProvider`
    /// trait doesn't require one), so this can't be `#[derive(Debug)]`d.
    /// Prints tool names, not full specs, to stay short in a turn-level log.
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("DelegateDescriptor")
            .field("agent_id", &self.agent_id)
            .field("provider_name", &self.provider_name)
            .field("model", &self.model)
            .field(
                "tool_specs",
                &self.tool_specs.iter().map(|s| &s.name).collect::<Vec<_>>(),
            )
            .field("max_steps", &self.max_steps)
            .finish_non_exhaustive()
    }
}

/// The trailing name segment of a `target_agent_id` / descriptor `agent_id`,
/// mirroring [`crate::handoff`]'s equivalent (own copy — see that module for
/// why the tolerant match exists: an operator may author either a bare name
/// or a namespaced `agent:ns/name` ref).
fn trailing_name(entry: &str) -> &str {
    entry.rsplit('/').next().unwrap_or(entry)
}

/// Find the [`DelegateDescriptor`] matching `target_agent_id` by trailing
/// name.
#[must_use]
pub fn find_descriptor<'a>(
    descriptors: &'a [DelegateDescriptor],
    target_agent_id: &str,
) -> Option<&'a DelegateDescriptor> {
    let target = trailing_name(target_agent_id);
    descriptors
        .iter()
        .find(|d| trailing_name(&d.agent_id) == target)
}

/// Parsed `__delegate_to` arguments, produced by [`parse_delegate_args`].
#[derive(Debug, Clone)]
pub struct DelegateRequest {
    /// Tool-call id the provider assigned to the `__delegate_to` call. Echoed
    /// back as the `tool_result` id so the function-calling loop sees a
    /// matched call → result pair.
    pub call_id: String,
    /// The model's chosen worker agent identifier.
    pub target_agent_id: String,
    /// The self-contained task for the worker to perform — becomes the sole
    /// user message of the worker's fresh transcript.
    pub task: String,
    /// Optional extra context, appended to the worker's transcript alongside
    /// `task`. `None` when the model supplied none.
    pub context: Option<String>,
    /// Optional JSON Schema the worker's final answer must satisfy (`#871`).
    /// `None` ⇒ the worker answers in free text under the
    /// [`WORKER_CONDENSATION_CONTRACT`] appended to its instructions
    /// (INV-C25, `#1140`). The raw schema value is not validated for well-formedness
    /// here (compiling it into a [`jsonschema::Validator`] is the caller's
    /// job, at the point it's actually used) — a bad schema is an argument
    /// error the caller surfaces the same way a missing `task` is.
    pub result_schema: Option<serde_json::Value>,
    /// Parent-workspace paths to seed into the worker's own workspace before
    /// its nested turn starts (`#2295`). Each entry is a literal relative path
    /// — a file, or a directory seeded recursively — and every one must fall
    /// inside the target agent's [`DelegateDescriptor::share_in`] ceiling.
    /// Empty when the model named none, which leaves the worker on the empty
    /// scratch space `#2286` gives it.
    pub share_in: Vec<String>,
}

/// Parse the JSON arguments of a `__delegate_to` tool call into a structured
/// [`DelegateRequest`].
///
/// Returns `None` if `args_json` doesn't parse, or either required field
/// (`target_agent_id`, `task`) is missing or empty — the caller then
/// surfaces a legible tool-result error rather than dispatching a malformed
/// delegation.
#[must_use]
pub fn parse_delegate_args(call_id: &str, args_json: &str) -> Option<DelegateRequest> {
    let v: serde_json::Value = serde_json::from_str(args_json).ok()?;
    let target_agent_id = v.get("target_agent_id")?.as_str()?.to_owned();
    if target_agent_id.is_empty() {
        return None;
    }
    let task = v.get("task")?.as_str()?.to_owned();
    if task.is_empty() {
        return None;
    }
    let context = v
        .get("context")
        .and_then(serde_json::Value::as_str)
        .filter(|s| !s.is_empty())
        .map(str::to_owned);
    let result_schema = v.get("result_schema").cloned();
    // A non-array `share_in`, or an array with non-string members, yields an
    // empty request rather than failing the parse: the delegation still has
    // everything it needs to run, and a worker that starts unseeded fails
    // visibly on its task instead of the orchestrator losing the whole call to
    // an argument-shape error. Empty entries are dropped — they would resolve
    // to the workspace root and seed everything.
    let share_in = v
        .get("share_in")
        .and_then(serde_json::Value::as_array)
        .map(|entries| {
            entries
                .iter()
                .filter_map(serde_json::Value::as_str)
                .filter(|s| !s.is_empty())
                .map(str::to_owned)
                .collect()
        })
        .unwrap_or_default();
    Some(DelegateRequest {
        call_id: call_id.to_owned(),
        target_agent_id,
        task,
        context,
        result_schema,
        share_in,
    })
}

#[cfg(test)]
mod tests {
    #![allow(clippy::pedantic, clippy::nursery, missing_docs)]

    use super::*;

    fn descriptor(agent_id: &str) -> DelegateDescriptor {
        DelegateDescriptor {
            agent_id: agent_id.to_owned(),
            instructions: None,
            provider: polyc_llm::into_dyn(polyc_llm::turn::StubProvider),
            provider_name: "stub".to_owned(),
            model: "stub".to_owned(),
            tool_specs: Vec::new(),
            max_steps: 4,
            native_search_allowed: false,
            share_in: ShareInCeiling::default(),
        }
    }

    #[test]
    fn parses_minimum_required_args() {
        let req = parse_delegate_args(
            "c-1",
            r#"{"target_agent_id":"researcher","task":"find the answer"}"#,
        )
        .unwrap();
        assert_eq!(req.target_agent_id, "researcher");
        assert_eq!(req.task, "find the answer");
        assert!(req.context.is_none());
        assert_eq!(req.call_id, "c-1");
    }

    #[test]
    fn parses_optional_context() {
        let req = parse_delegate_args(
            "c-2",
            r#"{"target_agent_id":"x","task":"t","context":"extra"}"#,
        )
        .unwrap();
        assert_eq!(req.context.as_deref(), Some("extra"));
    }

    #[test]
    fn parses_optional_result_schema() {
        let req = parse_delegate_args(
            "c-3",
            r#"{"target_agent_id":"x","task":"t","result_schema":{"type":"object"}}"#,
        )
        .unwrap();
        assert_eq!(
            req.result_schema,
            Some(serde_json::json!({"type":"object"}))
        );
    }

    #[test]
    fn result_schema_absent_by_default() {
        let req = parse_delegate_args("c-4", r#"{"target_agent_id":"x","task":"t"}"#).unwrap();
        assert!(req.result_schema.is_none());
    }

    #[test]
    fn rejects_missing_target_agent_id() {
        assert!(parse_delegate_args("c", r#"{"task":"t"}"#).is_none());
    }

    #[test]
    fn rejects_empty_target_agent_id() {
        assert!(parse_delegate_args("c", r#"{"target_agent_id":"","task":"t"}"#).is_none());
    }

    #[test]
    fn rejects_missing_task() {
        assert!(parse_delegate_args("c", r#"{"target_agent_id":"x"}"#).is_none());
    }

    #[test]
    fn rejects_empty_task() {
        assert!(parse_delegate_args("c", r#"{"target_agent_id":"x","task":""}"#).is_none());
    }

    #[test]
    fn rejects_garbage_json() {
        assert!(parse_delegate_args("c", "not-json").is_none());
    }

    #[test]
    fn delegate_tool_spec_has_required_fields() {
        let spec = delegate_tool_spec();
        assert_eq!(spec.name, DELEGATE_TOOL_NAME);
        let required = spec
            .schema_json
            .get("required")
            .and_then(|v| v.as_array())
            .cloned()
            .unwrap_or_default();
        assert!(required.iter().any(|v| v == "target_agent_id"));
        assert!(required.iter().any(|v| v == "task"));
        // #1141: strict schema — no undeclared arguments.
        assert_eq!(
            spec.schema_json.get("additionalProperties"),
            Some(&serde_json::json!(false))
        );
    }

    #[test]
    fn find_descriptor_matches_by_trailing_name() {
        let descriptors = vec![descriptor("agent:default/researcher"), descriptor("coder")];
        assert!(find_descriptor(&descriptors, "researcher").is_some());
        assert!(find_descriptor(&descriptors, "agent:other-ns/researcher").is_some());
        assert!(find_descriptor(&descriptors, "coder").is_some());
        assert!(find_descriptor(&descriptors, "ghost").is_none());
    }

    // ── #1140 / INV-C25: `worker_system_text` ────────────────────────────────
    //
    // TEST-17 (CONF-17), as direct unit tests of the pure composition helper
    // (PR #1152 review finding) rather than round-tripping a full worker turn
    // through a provider-capture harness — the schema×instructions matrix
    // lives entirely in this one function.

    /// TEST-17, first half: no `result_schema` ⇒ the contract is appended
    /// after the descriptor's own instructions.
    #[test]
    fn contract_appended_after_instructions_without_schema() {
        let text = worker_system_text(Some("You are a scoped worker."), false)
            .expect("no-schema path always returns Some");
        assert_eq!(
            text,
            format!("You are a scoped worker.\n\n{WORKER_CONDENSATION_CONTRACT}")
        );
    }

    /// TEST-17 corollary: no instructions of its own and no `result_schema`
    /// ⇒ the contract alone — a worker is never dispatched untold that its
    /// final message is the sole return channel.
    #[test]
    fn contract_alone_without_instructions_or_schema() {
        let text = worker_system_text(None, false).expect("no-schema path always returns Some");
        assert_eq!(text, WORKER_CONDENSATION_CONTRACT);
    }

    /// TEST-17, second half: with a `result_schema` in force, the
    /// schema-forced finalize path satisfies INV-C25 instead — the contract
    /// text is NOT injected and the descriptor's own instructions pass
    /// through unchanged.
    #[test]
    fn instructions_unchanged_with_schema() {
        let text = worker_system_text(Some("You are a scoped worker."), true);
        assert_eq!(text.as_deref(), Some("You are a scoped worker."));
    }

    /// With a `result_schema` in force AND no instructions, there is nothing
    /// to inject or pass through — no system message at all.
    #[test]
    fn no_system_text_with_schema_and_no_instructions() {
        assert_eq!(worker_system_text(None, true), None);
    }

    // ── #1323: `worker_turn_start_block` ────────────────────────────────────

    // Keep in lockstep with
    // `turn_start_block_labels_the_start_time_states_utc_and_omits_on_derivation_failure`
    // (crates/control-plane/src/grpc/tests.rs) — that test pins the same full
    // string for the same input ms against `turn_start_block`, the top-level
    // renderer this one deliberately mirrors.
    #[test]
    fn renders_utc_at_minute_precision() {
        // 2024-05-17T09:33:59Z, truncated to its own minute (never rounded).
        let block = worker_turn_start_block(1_715_938_439_000).expect("in-range");
        assert_eq!(
            block,
            "This turn started at 2024-05-17 09:33 UTC. Later steps in this turn may run after \
             this instant."
        );
    }

    #[test]
    fn matches_the_top_level_blocks_wording() {
        // Mirrors `polyc_control_plane::grpc::turn_start_block`'s phrasing
        // exactly — "started" framing, never "now".
        let block = worker_turn_start_block(0).expect("epoch is in range");
        assert!(block.starts_with("This turn started at "));
        assert!(!block.to_lowercase().contains("now"));
    }

    #[test]
    fn out_of_range_instant_renders_no_block() {
        assert_eq!(worker_turn_start_block(u64::MAX), None);
    }

    #[test]
    fn same_input_ms_renders_identical_bytes() {
        // Determinism: replaying the same recorded dispatch clock must
        // reproduce the exact same stamp, never a fresh clock's drift.
        let a = worker_turn_start_block(1_715_938_439_000);
        let b = worker_turn_start_block(1_715_938_439_000);
        assert_eq!(a, b);
    }
}