- 222
/// Stopping requires an observable effect. An effectful act that produced - 223
/// no effect did not finish, whatever the model says about it. - 224
Effect, - 225
/// Stopping requires a criterion to have been evaluated. - 226
Verification, - 227
} - 228
- 229
impl StopProfile { - 230
/// Strictest last. Composition takes the strictest. - 231
pub fn rank(self) -> u8 { - 232
match self { - 233
StopProfile::Message => 0, - 234
StopProfile::Inspection => 1, - 235
StopProfile::Effect => 2, - 236
StopProfile::Verification => 3, - 237
} - 238
} - 239
- 240
pub fn as_str(self) -> &'static str { - 241
match self { - 242
StopProfile::Message => "message", - 243
StopProfile::Inspection => "inspection", - 244
StopProfile::Effect => "effect", - 245
StopProfile::Verification => "verification", - 246
} - 247
} - 248
} - 249
- 250
/// How much history and recall this turn wants. - 251
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] - 252
#[serde(rename_all = "kebab-case")] - 253
pub enum ContextProfile { - 254
/// Just the conversation. No retrieval, no workspace scan. - 255
Minimal, - 256
/// Conversation plus recall of prior sessions and memory. - 257
Recall, - 258
/// Recall plus the workspace delta since the run started. - 259
Working, - 260
/// Everything, rendered from the commitment ledger. - 261
Full, - 262
} - 263
- 264
impl ContextProfile { - 265
/// Widest last. Composition takes the widest, since a strand that needs - 266
/// the workspace delta needs it whatever its neighbours need. - 267
pub fn rank(self) -> u8 { - 268
match self { - 269
ContextProfile::Minimal => 0, - 270
ContextProfile::Recall => 1, - 271
ContextProfile::Working => 2, - 272
ContextProfile::Full => 3, - 273
} - 274
} - 275
- 276
pub fn as_str(self) -> &'static str { - 277
match self { - 278
ContextProfile::Minimal => "minimal", - 279
ContextProfile::Recall => "recall", - 280
ContextProfile::Working => "working", - 281
ContextProfile::Full => "full", - 282
} - 283
} - 284
} - 285
- 286
/// The non-authority half of an engagement. - 287
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] - 288
pub struct Posture { - 289
/// Run under managed-work admission rather than a direct turn. - 290
pub managed: bool, - 291
/// Open a durable commitment for this work. - 292
pub open_commitment: bool, - 293
/// Take a checkpoint before the first effect. - 294
pub checkpoint_before_effect: bool, - 295
pub hil: HilMode, - 296
pub gate_fallback: GateFallback, - 297
pub clarify: ClarifyPolicy, - 298
pub delivery: DeliveryPosture, - 299
pub demand: DemandHint, - 300
pub stop: StopProfile, - 301
pub context: ContextProfile, - 302
/// The epistemic cognitive stance for this turn. - 303
#[serde(default)] - 304
pub epistemic_stance: EpistemicStance, - 305
/// The code-owned block this engagement contributes to the prompt. - 306
/// `None` when there is nothing worth spending tokens to say. - 307
#[serde(default, skip_serializing_if = "Option::is_none")] - 308
pub note: Option<String>, - 309
} - 310
- 311
/// What the runtime will do about a reading. - 312
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] - 313
pub struct Engagement { - 314
pub limits: Limits, - 315
pub posture: Posture, - 316
} - 317
- 318
impl Engagement { - 319
/// The engagement that changes nothing — vak's pre-kernel behaviour. - 320
pub fn general() -> Self { - 321
Engagement { - 322
limits: Limits::unrestricted(), - 323
posture: Posture { - 324
managed: false, - 325
open_commitment: false, - 326
checkpoint_before_effect: false, - 327
hil: HilMode::Interrupt, - 328
gate_fallback: GateFallback::Deny, - 329
clarify: ClarifyPolicy::Proceed, - 330
delivery: DeliveryPosture { - 331
shape: OutputShape::Prose, - 332
cadence: Cadence::Live, - 333
urgency: Urgency::Notify, - 334
}, - 335
demand: DemandHint { - 336
reasoning_required: false, - 337
evidence_required: false, - 338
structured_output: false, - 339
}, - 340
stop: StopProfile::Message, - 341
context: ContextProfile::Recall, - 342
epistemic_stance: EpistemicStance::DirectAnswer, - 343
note: None, - 344
}, - 345
} - 346
} - 347
- 348
/// The engagement for a request the kernel could not read confidently. - 349
/// - 350
/// Posture is the general one, but the tool surface is narrowed to the - 351
/// orientation floor (docs/design/68-context-engine.md, Principle 6: - 352
/// "when a decision cannot be made confidently, send less and give the - 353
/// model a way to ask for more"). This is an *explicit* decision rather - 354
/// than an ambiguous top element: `Limits::unrestricted` keeps meaning - 355
/// "everything", which is what a disabled kernel produces. - 356
pub fn orienting() -> Self { - 357
let mut engagement = Engagement::general(); - 358
engagement.limits.required_domains = - 359
crate::limits::DomainSet::only(FLOOR_DOMAINS.iter().copied()); - 360
engagement - 361
} - 362
- 363
/// Compose with another engagement, narrowing only: limits meet, and the - 364
/// posture takes the more cautious of each selection ([`Posture::meet`]). - 365
pub fn meet(&self, other: &Engagement) -> Engagement { - 366
Engagement { - 367
limits: self.limits.meet(&other.limits), - 368
posture: self.posture.meet(&other.posture), - 369
} - 370
} - 371
- 372
/// The engagement for a turn made of several strands. - 373
/// - 374
/// Everything authority-bearing meets: the strictest strand governs - 375
/// approval, permission, spend, closure evidence, modalities, and the - 376
/// posture. The domain requirement is the turn's *capacity* rather than - 377
/// its authority and takes the **union** — a turn that is "search the web, - 378
/// then run the tests" needs both toolsets. It is still at most the - 379
/// unrestricted baseline, which is the invariant that matters. - 380
pub fn compose(strands: &[Engagement]) -> Engagement { - 381
let Some((first, rest)) = strands.split_first() else { - 382
return Engagement::general(); - 383
}; - 384
let mut out = first.clone(); - 385
let mut domains = first.limits.required_domains.clone(); - 386
for next in rest { - 387
domains = domains.union(&next.limits.required_domains); - 388
out = out.meet(next); - 389
} - 390
out.limits.required_domains = domains; - 391
out - 392
} - 393
} - 394
- 395
impl Posture { - 396
/// The more cautious of two postures, field by field, under an explicit - 397
/// per-field order, so the operation is commutative and never less - 398
/// cautious than either operand. The delivery shape and the stance have - 399
/// no order and are `self`'s. Notes are kept from both sides: a - 400
/// model-visible instruction one strand needed is not dropped because - 401
/// another had none. - 402
pub fn meet(&self, other: &Posture) -> Posture { - 403
let a = self; - 404
let b = other; - 405
{ - 406
Posture { - 407
managed: a.managed || b.managed, - 408
open_commitment: a.open_commitment || b.open_commitment, - 409
checkpoint_before_effect: a.checkpoint_before_effect || b.checkpoint_before_effect, - 410
hil: if b.hil.caution_rank() > a.hil.caution_rank() { - 411
b.hil - 412
} else { - 413
a.hil - 414
}, - 415
gate_fallback: if a.gate_fallback == GateFallback::Deny - 416
|| b.gate_fallback == GateFallback::Deny - 417
{ - 418
GateFallback::Deny - 419
} else { - 420
GateFallback::Defer - 421
}, - 422
clarify: if b.clarify.caution_rank() > a.clarify.caution_rank() { - 423
b.clarify - 424
} else { - 425
a.clarify - 426
}, - 427
delivery: DeliveryPosture { - 428
// The primary strand's shape; shapes have no order. - 429
shape: a.delivery.shape, - 430
cadence: if b.delivery.cadence.immediacy_rank() - 431
> a.delivery.cadence.immediacy_rank() - 432
{ - 433
b.delivery.cadence - 434
} else { - 435
a.delivery.cadence - 436
}, - 437
urgency: if b.delivery.urgency.rank() > a.delivery.urgency.rank() { - 438
b.delivery.urgency - 439
} else { - 440
a.delivery.urgency - 441
}, - 442
}, - 443
demand: DemandHint { - 444
reasoning_required: a.demand.reasoning_required || b.demand.reasoning_required, - 445
evidence_required: a.demand.evidence_required || b.demand.evidence_required, - 446
structured_output: a.demand.structured_output || b.demand.structured_output, - 447
}, - 448
stop: if b.stop.rank() > a.stop.rank() { - 449
b.stop - 450
} else { - 451
a.stop - 452
}, - 453
context: if b.context.rank() > a.context.rank() { - 454
b.context - 455
} else { - 456
a.context - 457
}, - 458
// The primary strand's stance. Stances have no order either; - 459
// the strand-aware note carries the rest. - 460
epistemic_stance: a.epistemic_stance, - 461
note: match (&a.note, &b.note) { - 462
(None, None) => None, - 463
(Some(n), None) | (None, Some(n)) => Some(n.clone()), - 464
(Some(x), Some(y)) if x == y => Some(x.clone()), - 465
(Some(x), Some(y)) => Some(format!("{x}\n{y}")), - 466
}, - 467
} - 468
} - 469
} - 470
} - 471
- 472
// ------------------------------------------------------ capability slice --- - 473
- 474
/// Kinds of work this act plausibly needs, as domain names. - 475
/// - 476
/// This replaces a table that listed built-in *tool names* per act - 477
/// (`Act::Answer => ["webfetch", "mcp"]`). That shape could not answer the - 478
/// only question that matters — which of the capabilities this user actually - 479
/// installed could serve this request — because installed capabilities were - 480
/// never in it. Its failure mode was silent: a live-data question about a - 481
/// specific place read as `Answer`, sliced to six file tools, and came back - 482
/// "I do not have access to real-time information" with a configured, - 483
/// connected search server sitting right there. Naming the server in the - 484
/// next turn did not help either, because `Locate` had no way to reach one. - 485
/// - 486
/// Domains are matched against what each capability declares it serves, so - 487
/// adding an integration never edits this function. A capability that - 488
/// declares nothing is never narrowed away, which keeps the common case - 489
/// working with no configuration at all. - 490
fn act_domains(act: Act) -> &'static [&'static str] { - 491
match act { - 492
// A greeting needs nothing beyond the orientation floor. This is - 493
// where slicing pays for itself most obviously. - 494
Act::Converse => &[], - 495
// The acts that exist to produce a fact must be able to go and get - 496
// one. Lookup is read-only, and a missing capability costs a failed - 497
// task while an extra one costs a little context — so this trade is - 498
// the one this table has always claimed to want to make. - 499
Act::Answer => &["live-data", "web"], - 500
Act::Locate => &["live-data", "web", "vcs"], - 501
Act::Analyze => &["live-data", "web", "code-exec", "vcs"], - 502
Act::Author => &["documents", "web"], - 503
Act::Modify => &["documents", "code-exec", "vcs"], - 504
// Operating reaches outside the workspace and legitimately needs the - 505
// broad set; the narrowing that matters for this act is the approval - 506
// floor, not the toolbox. - 507
Act::Operate => &[ - 508
"code-exec", - 509
"web", - 510
"live-data", - 511
"messaging", - 512
"documents", - 513
"orchestration", - 514
], - 515
Act::Verify => &["code-exec", "observability"], - 516
Act::Orchestrate => &["orchestration", "code-exec"], - 517
Act::Govern => &["memory", "orchestration", "documents", "observability"], - 518
} - 519
} - 520
- 521
/// The domains every turn gets regardless of act: enough to look at what is - 522
/// in front of it and recall what it already knows. - 523
/// - 524
/// An agent that cannot look at anything cannot correct a misread of its own - 525
/// task, so this floor is what makes slicing safe to attempt at all. - 526
pub const FLOOR_DOMAINS: &[&str] = &["filesystem", "memory"]; - 527
- 528
/// Every domain a capability can declare it serves — the shared vocabulary - 529
/// readings are matched against (`vak_core::capability::Domain` mirrors it, - 530
/// and a test there keeps the two equal). A tier-2/3 classifier is told to - 531
/// choose from exactly these, because a free-form subject tag ("weather") - 532
/// matches no capability and would key behaviour on a topic. - 533
pub const DOMAIN_VOCABULARY: &[&str] = &[ - 534
"live-data", - 535
"web", - 536
"filesystem", - 537
"code-exec", - 538
"memory", - 539
"messaging", - 540
"documents", - 541
"orchestration", - 542
"vcs", - 543
"observability", - 544
]; - 545
- 546
// ----------------------------------------------------------- derivation --- - 547
- 548
/// Turn a reading plus the authority in force into an engagement. - 549
/// - 550
/// `slice_capabilities` is separate from the reading's own confidence because - 551
/// the two failure modes are not symmetric: raising an approval floor on a - 552
/// misread is a small annoyance, while removing a tool the task needed looks - 553
/// to the user like the agent is broken. Callers therefore gate slicing on a - 554
/// higher bar, and low confidence still tightens risk. - 555
pub fn derive(reading: &Reading, authority: &Authority, slice_capabilities: bool) -> Engagement { - 556
let mut limits = Limits::unrestricted(); - 557
- 558
// --- capabilities -------------------------------------------------- - 559
if slice_capabilities { - 560
// Union over every act the reading covers. A request that is - 561
// genuinely both a modification and a verification needs both - 562
// toolsets, and resolving the tie by argmax would silently remove - 563
// half of what it needs. - 564
let mut domains: BTreeSet<String> = FLOOR_DOMAINS - 565
.iter() - 566
.map(|domain| (*domain).to_string()) - 567
.collect(); - 568
for act in reading.acts() { - 569
domains.extend(act_domains(act).iter().map(|d| (*d).to_string())); - 570
} - 571
// Evidence the reading demands has to come from somewhere. A turn - 572
// required to cite cannot satisfy that from memory, so requiring - 573
// citation implies the ability to reach a source — whatever the act - 574
// was read as. This is the general form of the live-data failure: it - 575
// was never specific to `Answer`. - 576
if reading.evidence.rank() >= Evidence::Cited.rank() { - 577
domains.insert("live-data".into()); - 578
domains.insert("web".into()); - 579
} - 580
limits.required_domains = crate::limits::DomainSet::only(domains); - 581
} else { - 582
// Not confident enough to say what the turn needs: send the - 583
// orientation floor and let `find_tools` reach the rest (design 68, - 584
// Principle 6). Stated explicitly so `DomainSet::All` keeps its one - 585
// meaning — "everything", the disabled-kernel behaviour. - 586
limits.required_domains = crate::limits::DomainSet::only(FLOOR_DOMAINS.iter().copied()); - 587
} - 588
- 589
// --- modality ------------------------------------------------------ - 590
// A leg that cannot see is not a valid fallback for a vision turn. - 591
limits.required_modalities = reading.required_modalities(); - 592
- 593
// A reading never caps the route ladder, the turn count or the workers. - 594
// Those caps only ever removed capacity from requests the reader got - 595
// wrong — a 20-character "fix the failing test" lost its fallback legs - 596
// and gave its workers two steps — and a reading decides what is loaded, - 597
// never what is possible (invariant 32). - 598
- 599
// --- closure ------------------------------------------------------- - 600
limits.min_satisfaction = reading.evidence.min_satisfaction(); - 601
- 602
// --- posture ------------------------------------------------------- - 603
let hil = derive_hil(reading, authority); - 604
let delivery = derive_delivery(reading); - 605
let posture = Posture { - 606
// Multi-step work runs under a plan; only work that outlives the - 607
// session earns a durable commitment. - 608
managed: reading.horizon.rank() >= Horizon::Session.rank(), - 609
open_commitment: reading.horizon.opens_commitment(), - 610
// Any act the reading covers, not just the primary one. "migrate … - 611
// and verify … before deploying" resolves `verify` as primary, and - 612
// gating on that alone skipped the checkpoint for a turn that plainly - 613
// modifies and deploys. `Author` is deliberately not one of the acts - 614
// `requires_execution` covers (see `Act::requires_execution`): most - 615
// authoring never touches the workspace, and only the request text — - 616
// which this `Reading`-only derivation cannot see — says whether it - 617
// names a file. `OutcomeSpec::requires_execution` covers that case - 618
// once the objective text is known; a caller deciding whether to - 619
// checkpoint a file-saving authoring turn needs that check too. - 620
checkpoint_before_effect: reading.acts().iter().any(|act| act.requires_execution()) - 621
&& reading.stakes.wants_checkpoint(), - 622
hil, - 623
gate_fallback: GateFallback::Deny, - 624
clarify: derive_clarify(reading), - 625
delivery, - 626
demand: DemandHint { - 627
reasoning_required: matches!( - 628
reading.act, - 629
Act::Analyze | Act::Author | Act::Modify | Act::Orchestrate - 630
) || reading.horizon.rank() >= Horizon::Session.rank(), - 631
evidence_required: reading.evidence.rank() >= Evidence::Cited.rank(), - 632
structured_output: matches!( - 633
delivery.shape, - 634
OutputShape::Matrix | OutputShape::Table | OutputShape::Diff - 635
), - 636
}, - 637
stop: derive_stop(reading), - 638
context: derive_context(reading), - 639
epistemic_stance: derive_epistemic_stance(reading), - 640
note: derive_note(reading, hil), - 641
}; - 642
- 643
let mut engagement = Engagement { limits, posture }; - 644
apply_authority(&mut engagement, reading, authority); - 645
engagement - 646
} - 647
- 648
/// Apply the authority in force to an engagement: the approval ceiling for - 649
/// the reading's stakes, and what happens to a gate nobody can answer. - 650
/// - 651
/// One place for every path — a confident reading, a provisional one, and a - 652
/// part the free tiers could not read at all — because authority does not - 653
/// depend on how well the request was read: `manual` means "ask" whether or - 654
/// not the reader understood the words. - 655
/// - 656
/// A grant on a commitment is applied separately, once the strands serving - 657
/// it are known (`apply_envelopes`), and covers actions one at a time at the - 658
/// approval gate — an engagement is computed before anyone knows which paths - 659
/// a turn will touch. - 660
pub fn apply_authority(engagement: &mut Engagement, reading: &Reading, authority: &Authority) { - 661
engagement.limits.approval_ceiling = authority.approval_ceiling(reading.stakes); - 662
engagement.posture.gate_fallback = authority.gate_fallback(reading.horizon); - 663
} - 664
- 665
/// Derive the appropriate epistemic cognitive stance from the reading. - 666
/// - 667
/// Ensures the model adopts the right operational and intellectual posture - 668
/// across any domain of work without domain-specific stereotyping. - 669
pub fn derive_epistemic_stance(reading: &Reading) -> EpistemicStance { - 670
match reading.act { - 671
Act::Converse => EpistemicStance::Conversational, - 672
Act::Answer => { - 673
if reading.evidence.rank() >= Evidence::Cited.rank() { - 674
EpistemicStance::Analytical - 675
} else { - 676
EpistemicStance::DirectAnswer - 677
} - 678
} - 679
Act::Locate => EpistemicStance::Exploratory, - 680
Act::Analyze => EpistemicStance::Analytical, - 681
Act::Author => EpistemicStance::Generative, - 682
Act::Modify | Act::Operate | Act::Govern => EpistemicStance::Operational, - 683
Act::Verify => EpistemicStance::Diagnostic, - 684
Act::Orchestrate => { - 685
if reading.acts().iter().any(|act| act.is_effectful()) { - 686
EpistemicStance::Operational - 687
} else { - 688
EpistemicStance::Exploratory - 689
} - 690
} - 691
} - 692
} - 693
- 694
fn derive_hil(reading: &Reading, authority: &Authority) -> HilMode { - 695
let needs_a_human = reading.stakes.rank() >= Stakes::Costly.rank(); - 696
// Nobody to ask and somewhere to park the question: wait rather than - 697
// fail. This covers irreversible work too — an irreversible step with - 698
// nobody present must reach the inbox, not raise a gate that can only - 699
// time out. "Interrupt" is what happens when someone is here to be - 700
// interrupted. - 701
if needs_a_human && authority.gate_fallback(reading.horizon) == GateFallback::Defer { - 702
return HilMode::Defer; - 703
} - 704
// Irreversible stakes always interrupt whatever was delegated. - 705
if reading.stakes == Stakes::Irreversible { - 706
return HilMode::Interrupt; - 707
} - 708
// Manual autonomy is "propose only": nothing is done first and shown - 709
// afterwards, so `Review` is never the right mode for it. - 710
if authority.autonomy == Autonomy::Manual { - 711
return HilMode::Interrupt; - 712
} - 713
// `Envelope` is set by `apply_envelopes` on a strand whose commitment - 714
// carries a live grant; delegation without one is not a boundary. - 715
if authority.autonomy == Autonomy::Autonomous { - 716
return HilMode::Review; - 717
} - 718
// Reversible work under a checkpoint is better reviewed than pre-approved. - 719
if reading.stakes.rank() <= Stakes::Reversible.rank() - 720
&& reading.acts().iter().any(|act| act.is_effectful()) - 721
{ - 722
return HilMode::Review; - 723
} - 724
HilMode::Interrupt - 725
} - 726
- 727
fn derive_clarify(reading: &Reading) -> ClarifyPolicy { - 728
// Ambiguity is only worth interrupting for when being wrong would hurt. - 729
let costly = reading.stakes.rank() >= Stakes::Costly.rank(); - 730
match reading.clarity { - 731
Clarity::Clear => ClarifyPolicy::Proceed, - 732
Clarity::Underspecified | Clarity::Ambiguous if costly => ClarifyPolicy::Ask, - 733
Clarity::Underspecified | Clarity::Ambiguous => ClarifyPolicy::StateAssumption, - 734
} - 735
} - 736
- 737
fn derive_delivery(reading: &Reading) -> DeliveryPosture { - 738
let shape = match reading.act { - 739
Act::Converse | Act::Answer => { - 740
if reading.evidence.rank() >= Evidence::Cited.rank() { - 741
OutputShape::Sources - 742
} else { - 743
OutputShape::Prose - 744
} - 745
} - 746
Act::Locate => OutputShape::Findings, - 747
Act::Analyze => { - 748
if reading.evidence.rank() >= Evidence::Cited.rank() { - 749
OutputShape::Sources - 750
} else { - 751
OutputShape::Prose - 752
} - 753
} - 754
Act::Author => OutputShape::Artifact, - 755
Act::Modify => OutputShape::Diff, - 756
Act::Operate | Act::Govern => OutputShape::Report, - 757
Act::Verify => OutputShape::Matrix, - 758
Act::Orchestrate => OutputShape::Report, - 759
}; - 760
let cadence = match reading.attendance { - 761
Attendance::Interactive => Cadence::Live, - 762
Attendance::Supervised => Cadence::OnCompletion, - 763
// Overnight work sends one roll-up, not a stream nobody is reading. - 764
Attendance::Unattended => Cadence::Digest, - 765
}; - 766
let urgency = match (reading.stakes, reading.attendance) { - 767
(Stakes::Irreversible, _) => Urgency::Interrupt, - 768
(Stakes::Costly, Attendance::Unattended) => Urgency::Notify, - 769
(_, Attendance::Unattended) => Urgency::Quiet, - 770
_ => Urgency::Notify, - 771
}; - 772
DeliveryPosture { - 773
shape, - 774
cadence, - 775
urgency, - 776
} - 777
} - 778
- 779
fn derive_stop(reading: &Reading) -> StopProfile { - 780
if reading.evidence.rank() >= Evidence::Verified.rank() { - 781
return StopProfile::Verification; - 782
} - 783
// The *primary* act, not every contender: a contender is a noun that - 784
// happens to be a verb somewhere ("what do we know about deploys?" - 785
// carries an `operate` contender), and gating the stop on it demanded - 786
// an effect from a question. A request that genuinely has an effectful - 787
// part is a strand of its own, and the composite stop is the strictest - 788
// strand's. If the act changes something, an episode that changed - 789
// nothing did not finish, whatever the model says about it. - 790
if reading.act.is_effectful() { - 791
return StopProfile::Effect; - 792
} - 793
match reading.act { - 794
Act::Converse | Act::Answer => StopProfile::Message, - 795
_ => StopProfile::Inspection, - 796
} - 797
} - 798
- 799
fn derive_context(reading: &Reading) -> ContextProfile { - 800
if reading.horizon.opens_commitment() { - 801
return ContextProfile::Full; - 802
} - 803
if reading - 804
.acts() - 805
.iter() - 806
.any(|act| matches!(act, Act::Modify | Act::Verify | Act::Operate)) - 807
{ - 808
return ContextProfile::Working; - 809
} - 810
match reading.act { - 811
// A greeting does not need last Tuesday's session searched. - 812
Act::Converse => ContextProfile::Minimal, - 813
_ => ContextProfile::Recall, - 814
} - 815
} - 816
- 817
/// The code-owned prompt block, when there is something worth saying. - 818
/// - 819
/// Deliberately terse and factual. This is not a place to re-explain the - 820
/// agent's job; it states the few decisions the model cannot otherwise see, - 821
/// and stays silent when there are none. - 822
fn derive_note(reading: &Reading, hil: HilMode) -> Option<String> { - 823
let mut lines = Vec::new(); - 824
match derive_clarify(reading) { - 825
ClarifyPolicy::StateAssumption => lines.push( - 826
"This request is under-specified. Choose the most reasonable reading, \ - 827
state the assumption you made in one sentence, and proceed." - 828
.to_string(), - 829
), - 830
ClarifyPolicy::Ask => lines.push( - 831
"This request is ambiguous and the stakes are high enough that guessing \ - 832
is not acceptable. Ask one specific question before acting." - 833
.to_string(), - 834
), - 835
ClarifyPolicy::Proceed => {} - 836
} - 837
if reading.evidence.rank() >= Evidence::Verified.rank() { - 838
lines.push(format!( - 839
"Completion requires {} evidence: the runtime, not you, decides whether \ - 840
this is done. Produce a checkable result and do not claim success \ - 841
without one.", - 842
reading.evidence.min_satisfaction().as_str() - 843
)); - 844
} else if reading.evidence == Evidence::Cited { - 845
lines.push("Cite the sources behind any factual claim.".to_string()); - 846
} - 847
if hil == HilMode::Defer { - 848
lines.push( - 849
"Nobody is available to answer right now. If you need a decision, say so \ - 850
plainly and stop; the question will be queued rather than guessed." - 851
.to_string(), - 852
); - 853
} - 854
if reading.stakes == Stakes::Irreversible { - 855
lines.push(if hil == HilMode::Defer { - 856
"At least one step here cannot be undone and nobody is available to \ - 857
confirm it. Do everything up to that step, then stop and say what \ - 858
needs confirming." - 859
.to_string() - 860
} else { - 861
"At least one step here cannot be undone. Confirm before that step, \ - 862
not after." - 863
.to_string() - 864
}); - 865
} - 866
if lines.is_empty() { - 867
None - 868
} else { - 869
Some(lines.join("\n")) - 870
} - 871
} - 872
- 873
/// Modalities a reading needs, exposed for the ladder filter. - 874
pub fn required_modalities(reading: &Reading) -> BTreeSet<Modality> { - 875
reading.required_modalities() - 876
} - 877
- 878
#[cfg(test)] - 879
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 880
mod tests { - 881
use super::*; - 882
use crate::axes::{Act, Attendance, Clarity, Evidence, Horizon, Stakes}; - 883
- 884
fn reading(act: Act, horizon: Horizon, stakes: Stakes, evidence: Evidence) -> Reading { - 885
Reading { - 886
act, - 887
horizon, - 888
stakes, - 889
evidence, - 890
clarity: Clarity::Clear, - 891
attendance: Attendance::Interactive, - 892
confidence: 0.9, - 893
..Reading::general() - 894
} - 895
} - 896
- 897
/// Invariant 1 across the entire reachable space of readings. If this ever - 898
/// fails, some derivation is granting rather than restricting. - 899
#[test] - 900
fn no_derived_engagement_ever_widens_the_baseline() { - 901
let baseline = Limits::unrestricted(); - 902
for act in Act::ALL { - 903
for horizon in Horizon::ALL { - 904
for stakes in Stakes::ALL { - 905
for evidence in Evidence::ALL { - 906
for clarity in Clarity::ALL { - 907
for attendance in Attendance::ALL { - 908
for autonomy in crate::Autonomy::ALL { - 909
for slice in [true, false] { - 910
let mut r = reading(act, horizon, stakes, evidence); - 911
r.clarity = clarity; - 912
r.attendance = attendance; - 913
let authority = Authority { - 914
autonomy, - 915
attendance, - 916
}; - 917
let engagement = derive(&r, &authority, slice); - 918
assert!( - 919
engagement.limits.is_at_most(&baseline), - 920
"widened: {act:?}/{horizon:?}/{stakes:?}/\ - 921
{evidence:?}/{autonomy:?} slice={slice}" - 922
); - 923
} - 924
} - 925
} - 926
} - 927
} - 928
} - 929
} - 930
} - 931
} - 932
- 933
#[test] - 934
fn a_greeting_gets_no_tools_and_minimal_context() { - 935
let r = reading( - 936
Act::Converse, - 937
Horizon::Immediate, - 938
Stakes::Inert, - 939
Evidence::None, - 940
); - 941
let engagement = derive(&r, &Authority::default(), true); - 942
// A greeting asks for nothing beyond the floor that lets it look at - 943
// what is in front of it. - 944
assert_eq!( - 945
engagement.limits.required_domains, - 946
crate::limits::DomainSet::only(FLOOR_DOMAINS.iter().copied()) - 947
); - 948
assert_eq!(engagement.posture.context, ContextProfile::Minimal); - 949
assert!(!engagement.posture.open_commitment); - 950
assert_eq!(engagement.posture.note, None); - 951
} - 952
- 953
#[test] - 954
fn every_act_keeps_the_orientation_floor() { - 955
for act in Act::ALL { - 956
let r = reading(act, Horizon::Turn, Stakes::Reversible, Evidence::None); - 957
let engagement = derive(&r, &Authority::default(), true); - 958
for domain in FLOOR_DOMAINS { - 959
assert!( - 960
engagement.limits.required_domains.contains(domain), - 961
"{act:?} lost `{domain}` and cannot orient itself" - 962
); - 963
} - 964
} - 965
} - 966
- 967
#[test] - 968
fn asking_and_searching_can_still_reach_a_live_source() { - 969
// Regression: `answer` and `locate` were sliced to the orientation - 970
// floor and `webfetch`. A configured search MCP server (Tavily) was - 971
// therefore unreachable for exactly the two acts that ask for a - 972
// fact, and "how is the weather today" came back as "I do not have - 973
// access to real-time information" with the tool sitting right - 974
// there, connected and admitted. - 975
for act in [Act::Answer, Act::Locate] { - 976
let r = reading(act, Horizon::Turn, Stakes::Inert, Evidence::None); - 977
let engagement = derive(&r, &Authority::default(), true); - 978
assert!( - 979
engagement.limits.required_domains.contains("live-data"), - 980
"{act:?} cannot reach a live source" - 981
); - 982
} - 983
} - 984
- 985
#[test] - 986
fn required_citation_implies_reaching_a_source_whatever_the_act() { - 987
// The general form of the weather failure: it was never specific to - 988
// `Answer`. A turn obliged to cite cannot satisfy that from memory. - 989
let r = reading(Act::Verify, Horizon::Turn, Stakes::Inert, Evidence::Cited); - 990
let engagement = derive(&r, &Authority::default(), true); - 991
assert!(engagement.limits.required_domains.contains("live-data")); - 992
} - 993
- 994
#[test] - 995
fn domain_requirements_narrow_by_intersection() { - 996
// More domains admits more capabilities, so composing two limits - 997
// must take the intersection or `meet` would widen. - 998
let wide = Limits { - 999
required_domains: crate::limits::DomainSet::only(["web", "live-data", "code-exec"]), - 1000
..Limits::unrestricted() - 1001
}; - 1002
let narrow = Limits { - 1003
required_domains: crate::limits::DomainSet::only(["web"]), - 1004
..Limits::unrestricted() - 1005
}; - 1006
let met = wide.meet(&narrow); - 1007
assert_eq!( - 1008
met.required_domains, - 1009
crate::limits::DomainSet::only(["web"]) - 1010
); - 1011
assert!(met.is_at_most(&wide)); - 1012
assert!(!wide.is_at_most(&narrow), "widening must not validate"); - 1013
} - 1014
- 1015
#[test] - 1016
fn durable_work_is_managed_opens_a_commitment_and_renders_full_context() { - 1017
let r = reading( - 1018
Act::Modify, - 1019
Horizon::Durable, - 1020
Stakes::Reversible, - 1021
Evidence::Verified, - 1022
); - 1023
let engagement = derive(&r, &Authority::default(), true); - 1024
assert!(engagement.posture.managed); - 1025
assert!(engagement.posture.open_commitment); - 1026
assert_eq!(engagement.posture.context, ContextProfile::Full); - 1027
assert_eq!(engagement.posture.stop, StopProfile::Verification); - 1028
assert_eq!( - 1029
engagement.limits.min_satisfaction, - 1030
crate::Satisfaction::Observed - 1031
); - 1032
} - 1033
- 1034
#[test] - 1035
fn audited_work_cannot_be_closed_by_the_model_alone() { - 1036
let r = reading( - 1037
Act::Modify, - 1038
Horizon::Session, - 1039
Stakes::Reversible, - 1040
Evidence::Audited, - 1041
); - 1042
let engagement = derive(&r, &Authority::default(), true); - 1043
assert_eq!( - 1044
engagement.limits.min_satisfaction, - 1045
crate::Satisfaction::Attested - 1046
); - 1047
assert!(!crate::Satisfaction::Asserted.satisfies(engagement.limits.min_satisfaction)); - 1048
assert!(!crate::Satisfaction::Cited.satisfies(engagement.limits.min_satisfaction)); - 1049
} - 1050
- 1051
#[test] - 1052
fn low_stakes_ambiguity_states_an_assumption_and_high_stakes_asks() { - 1053
let mut low = reading(Act::Answer, Horizon::Turn, Stakes::Inert, Evidence::None); - 1054
low.clarity = Clarity::Ambiguous; - 1055
assert_eq!( - 1056
derive(&low, &Authority::default(), true).posture.clarify, - 1057
ClarifyPolicy::StateAssumption - 1058
); - 1059
- 1060
let mut high = reading( - 1061
Act::Operate, - 1062
Horizon::Turn, - 1063
Stakes::Irreversible, - 1064
Evidence::None, - 1065
); - 1066
high.clarity = Clarity::Ambiguous; - 1067
assert_eq!( - 1068
derive(&high, &Authority::default(), true).posture.clarify, - 1069
ClarifyPolicy::Ask - 1070
); - 1071
} - 1072
- 1073
#[test] - 1074
fn unattended_durable_costly_work_defers_rather_than_interrupting() { - 1075
let r = reading( - 1076
Act::Operate, - 1077
Horizon::Durable, - 1078
Stakes::Costly, - 1079
Evidence::None, - 1080
); - 1081
let authority = Authority { - 1082
autonomy: Autonomy::Delegated, - 1083
attendance: Attendance::Unattended, - 1084
}; - 1085
let engagement = derive(&r, &authority, true); - 1086
assert_eq!(engagement.posture.hil, HilMode::Defer); - 1087
assert_eq!(engagement.posture.gate_fallback, GateFallback::Defer); - 1088
} - 1089
- 1090
#[test] - 1091
fn unattended_work_rolls_up_instead_of_pinging() { - 1092
let mut r = reading(Act::Verify, Horizon::Durable, Stakes::Inert, Evidence::None); - 1093
r.attendance = Attendance::Unattended; - 1094
let delivery = derive(&r, &Authority::default(), true).posture.delivery; - 1095
assert_eq!(delivery.cadence, Cadence::Digest); - 1096
assert_eq!(delivery.urgency, Urgency::Quiet); - 1097
} - 1098
- 1099
#[test] - 1100
fn irreversible_work_always_interrupts_and_says_so_in_the_prompt() { - 1101
let r = reading( - 1102
Act::Operate, - 1103
Horizon::Turn, - 1104
Stakes::Irreversible, - 1105
Evidence::None, - 1106
); - 1107
let engagement = derive(&r, &Authority::default(), true); - 1108
assert_eq!(engagement.posture.hil, HilMode::Interrupt); - 1109
assert_eq!(engagement.posture.delivery.urgency, Urgency::Interrupt); - 1110
assert!( - 1111
engagement - 1112
.posture - 1113
.note - 1114
.as_deref() - 1115
.is_some_and(|n| n.contains("cannot be undone")) - 1116
); - 1117
} - 1118
- 1119
#[test] - 1120
fn demand_is_populated_rather_than_left_at_zero() { - 1121
let r = reading( - 1122
Act::Analyze, - 1123
Horizon::Session, - 1124
Stakes::Inert, - 1125
Evidence::Cited, - 1126
); - 1127
let demand = derive(&r, &Authority::default(), true).posture.demand; - 1128
assert!(demand.reasoning_required); - 1129
assert!(demand.evidence_required); - 1130
} - 1131
- 1132
/// A reading whose contenders include an effectful act must checkpoint; - 1133
/// the stop rule follows the primary act, and a compound request with a - 1134
/// genuinely effectful part carries it as a strand of its own. - 1135
#[test] - 1136
fn any_effectful_contender_forces_a_checkpoint_but_not_an_effect_stop() { - 1137
let mut r = reading( - 1138
Act::Verify, - 1139
Horizon::Session, - 1140
Stakes::Reversible, - 1141
Evidence::None, - 1142
); - 1143
r.alternate_acts.insert(Act::Modify); - 1144
let engagement = derive(&r, &Authority::default(), true); - 1145
assert!(engagement.posture.checkpoint_before_effect); - 1146
assert_eq!(engagement.posture.stop, StopProfile::Inspection); - 1147
- 1148
// And a reading with no effectful act does neither. - 1149
let inert = reading( - 1150
Act::Answer, - 1151
Horizon::Turn, - 1152
Stakes::Reversible, - 1153
Evidence::None, - 1154
); - 1155
let engagement = derive(&inert, &Authority::default(), true); - 1156
assert!(!engagement.posture.checkpoint_before_effect); - 1157
assert_eq!(engagement.posture.stop, StopProfile::Message); - 1158
} - 1159
- 1160
#[test] - 1161
fn meet_of_two_engagements_never_widens_either() { - 1162
let a = derive( - 1163
&reading( - 1164
Act::Modify, - 1165
Horizon::Session, - 1166
Stakes::Costly, - 1167
Evidence::Verified, - 1168
), - 1169
&Authority::default(), - 1170
true, - 1171
); - 1172
let b = derive( - 1173
&reading(Act::Answer, Horizon::Turn, Stakes::Inert, Evidence::None), - 1174
&Authority::default(), - 1175
true, - 1176
); - 1177
let met = a.meet(&b); - 1178
assert!(met.limits.is_at_most(&a.limits)); - 1179
assert!(met.limits.is_at_most(&b.limits)); - 1180
} - 1181
} - 1182
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.