lisa-pack-core 0.1.0__tar.gz

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.
Files changed (38) hide show
  1. lisa_pack_core-0.1.0/.gitignore +33 -0
  2. lisa_pack_core-0.1.0/PKG-INFO +10 -0
  3. lisa_pack_core-0.1.0/pyproject.toml +35 -0
  4. lisa_pack_core-0.1.0/src/sdk/packs/core/__init__.py +14 -0
  5. lisa_pack_core-0.1.0/src/sdk/packs/core/content/prompts/sub_turn/budget_exhausted.md +3 -0
  6. lisa_pack_core-0.1.0/src/sdk/packs/core/content/prompts/sub_turn/delegate_contract.md +8 -0
  7. lisa_pack_core-0.1.0/src/sdk/packs/core/content/prompts/sub_turn/fork_contract.md +8 -0
  8. lisa_pack_core-0.1.0/src/sdk/packs/core/content/prompts/sub_turn/midpoint_write_nudge.md +6 -0
  9. lisa_pack_core-0.1.0/src/sdk/packs/core/content/prompts/sub_turn/no_write_warning.md +4 -0
  10. lisa_pack_core-0.1.0/src/sdk/packs/core/content/prompts/system-prompt.md +9 -0
  11. lisa_pack_core-0.1.0/src/sdk/packs/core/content/prompts/thread_manifest_hint.md +37 -0
  12. lisa_pack_core-0.1.0/src/sdk/packs/core/content/redaction_patterns.yml +46 -0
  13. lisa_pack_core-0.1.0/src/sdk/packs/core/modes/default/_mode.md +17 -0
  14. lisa_pack_core-0.1.0/src/sdk/packs/core/modes/explainer/_mode.md +43 -0
  15. lisa_pack_core-0.1.0/src/sdk/packs/core/modes/planner/_mode.md +41 -0
  16. lisa_pack_core-0.1.0/src/sdk/packs/core/operations/__init__.py +1 -0
  17. lisa_pack_core-0.1.0/src/sdk/packs/core/operations/delegation/__init__.py +1 -0
  18. lisa_pack_core-0.1.0/src/sdk/packs/core/operations/delegation/delegate.md +61 -0
  19. lisa_pack_core-0.1.0/src/sdk/packs/core/operations/delegation/delegate.py +198 -0
  20. lisa_pack_core-0.1.0/src/sdk/packs/core/operations/delegation/delegate.yml +14 -0
  21. lisa_pack_core-0.1.0/src/sdk/packs/core/operations/delegation/fork.md +73 -0
  22. lisa_pack_core-0.1.0/src/sdk/packs/core/operations/delegation/fork.py +232 -0
  23. lisa_pack_core-0.1.0/src/sdk/packs/core/operations/delegation/fork.yml +15 -0
  24. lisa_pack_core-0.1.0/src/sdk/packs/core/operations/planning/__init__.py +1 -0
  25. lisa_pack_core-0.1.0/src/sdk/packs/core/operations/planning/plan_workflow.py +691 -0
  26. lisa_pack_core-0.1.0/src/sdk/packs/core/operations/planning/plan_workflow.yml +14 -0
  27. lisa_pack_core-0.1.0/src/sdk/packs/core/sessions/default.md +116 -0
  28. lisa_pack_core-0.1.0/src/sdk/packs/core/tools/__init__.py +0 -0
  29. lisa_pack_core-0.1.0/src/sdk/packs/core/tools/fetch_page/__init__.py +0 -0
  30. lisa_pack_core-0.1.0/src/sdk/packs/core/tools/fetch_page/_models.py +21 -0
  31. lisa_pack_core-0.1.0/src/sdk/packs/core/tools/fetch_page/fetch_page.py +251 -0
  32. lisa_pack_core-0.1.0/src/sdk/packs/core/tools/fetch_page/fetch_page.tool.yml +9 -0
  33. lisa_pack_core-0.1.0/tests/test_delegate_fn.py +186 -0
  34. lisa_pack_core-0.1.0/tests/test_delegate_operation.py +195 -0
  35. lisa_pack_core-0.1.0/tests/test_fetch_page_tool.py +112 -0
  36. lisa_pack_core-0.1.0/tests/test_fork_fn.py +204 -0
  37. lisa_pack_core-0.1.0/tests/test_fork_operation.py +381 -0
  38. lisa_pack_core-0.1.0/tests/test_plan_workflow_settings.py +26 -0
@@ -0,0 +1,33 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+
9
+
10
+ # Virtual environments
11
+ .venv/
12
+
13
+ # IDE
14
+ .vscode/
15
+
16
+ # OS
17
+ .DS_Store
18
+ Thumbs.db
19
+
20
+ # Testing
21
+ .import_linter_cache/
22
+ .pytest_cache/
23
+ .ruff_cache/
24
+ .mypy_cache/
25
+ .lisa-tmp/
26
+ coverage/
27
+ .coverage
28
+ .coverage.*
29
+ htmlcov/
30
+
31
+ # Outputs
32
+ artifacts/
33
+ .e2e/
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.5
2
+ Name: lisa-pack-core
3
+ Version: 0.1.0
4
+ Summary: Lisa core pack - baseline modes, operations, and authored content
5
+ Requires-Python: >=3.11
6
+ Provides-Extra: dev
7
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
8
+ Requires-Dist: pytest-cov>=6.0.0; extra == 'dev'
9
+ Requires-Dist: pytest-mock>=3.14.0; extra == 'dev'
10
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [tool.hatch.build.targets.wheel]
6
+ # sdk.packs.core lives under the shared sdk namespace; hatchling includes
7
+ # all files in the package dir by default (no package-data globs needed).
8
+ packages = ["src/sdk"]
9
+ only-include = ["src/sdk/packs/core"]
10
+
11
+ [project]
12
+ name = "lisa-pack-core"
13
+ version = "0.1.0"
14
+ description = "Lisa core pack - baseline modes, operations, and authored content"
15
+ requires-python = ">=3.11"
16
+
17
+ [project.optional-dependencies]
18
+ dev = [
19
+ "pytest>=8.0.0",
20
+ "pytest-asyncio>=0.23.0",
21
+ "pytest-cov>=6.0.0",
22
+ "pytest-mock>=3.14.0",
23
+ ]
24
+
25
+ [tool.pytest.ini_options]
26
+ pythonpath = ["src"]
27
+ testpaths = ["tests"]
28
+ markers = [
29
+ "live: hits real external signal or provider APIs and prints a tester-visible summary",
30
+ "integration: multi-module in-process test; may require Azurite on localhost:10000",
31
+ "eval: deterministic agent regression evals (no model calls, no I/O)",
32
+ ]
33
+
34
+ [project.entry-points."lisa.extensions.content_roots"]
35
+ core = "sdk.packs.core:get_content_root"
@@ -0,0 +1,14 @@
1
+ """Core baseline pack for Lisa.
2
+
3
+ Contains the shipped baseline modes, authored markdown assets, and reusable operations
4
+ that the host runtime loads through pack discovery.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+
11
+
12
+ def get_content_root() -> Path:
13
+ """Return the content root directory for this pack."""
14
+ return Path(__file__).parent
@@ -0,0 +1,3 @@
1
+ # Sub-turn budget-exhausted tail
2
+
3
+ BUDGET EXHAUSTED — call an action tool now or yield.
@@ -0,0 +1,8 @@
1
+ # Delegate-child sub-turn contract body
2
+
3
+ Subroutine child: success REQUIRES calling at least one of: [{tools_list}].
4
+ Read-only calls do NOT count. If the budget exhausts without a write, return
5
+ status=no_tools so your parent can decide — do not claim success.
6
+
7
+ {remaining_iterations} iterations remain to produce a write; reads are
8
+ scaffolding, not progress.
@@ -0,0 +1,8 @@
1
+ # Fork-child sub-turn contract body
2
+
3
+ Fork child: success REQUIRES calling at least one of: [{tools_list}]. Read-only
4
+ calls do NOT count. If the budget exhausts without a write, your output is
5
+ discarded as unverified — do not claim success.
6
+
7
+ {remaining_iterations} iterations remain to produce a write; reads are
8
+ scaffolding, not progress.
@@ -0,0 +1,6 @@
1
+ # Sub-turn midpoint write-nudge body
2
+
3
+ You are past the midpoint of your budget with zero writes. Call one of:
4
+ [{action_list}] eminently to complete your work, or call `yield` to hand back
5
+ honestly. Continuing to read without writing will result in your output being
6
+ discarded.
@@ -0,0 +1,4 @@
1
+ # No-verified-write warning footnote
2
+
3
+ ⚠️ This response produced no verified write;
4
+ available actions on the last turn were: {action_names}
@@ -0,0 +1,9 @@
1
+ # Lisa Harness
2
+
3
+ Operate according to the session context.
4
+ Everything before `## User Query` is session context.
5
+
6
+ ## Capabilities
7
+
8
+ If the request asks for an action no available operation or tool enacts,
9
+ and no mode routing hint satisfies, say so.
@@ -0,0 +1,37 @@
1
+ # Thread manifest hint
2
+
3
+ The manifest above is a table of contents of **past** turns.
4
+ Each row is a resolved exchange — do not re-raise, re-narrate, or continue issues from earlier rows.
5
+ The row marked `← current` is the turn the user is responding to; treat the rest as history.
6
+
7
+ ## When to pull prior turns
8
+
9
+ Pull prior turns when:
10
+
11
+ - The user references prior work ("that thing we did earlier", "the fix from yesterday").
12
+ - You need to verify a past decision before acting on a related request.
13
+ - The current query does not make sense without getting prior context.
14
+
15
+ ## How to find the right turns
16
+
17
+ - **Within this manifest window**: scan the table for queries matching the current topic. Use `thread_read_range` for the relevant turn numbers.
18
+ - **Tool results from earlier turns**: pass `include_transcript: true` to get tool call/result previews. Large values carry a `ref`, head/tail excerpts, and an omitted-character count; they are not full results. Use `thread_read_content(ref=...)` to expand only the value you need instead of calling the original tool again.
19
+ - **Beyond this manifest window** (turns older than the last 30): use `thread_search` with a keyword or phrase, then read the matching turns. Do not scan the whole thread to find a specific result.
20
+ - **Continue a read**: responses contain `entries`, `next_cursor`, and `has_more`. Follow `next_cursor` using the same tool and inputs. Expansion pages use `next_offset_chars` with the same `ref`; offsets count Unicode characters after redaction. The complete permitted content remains recoverable from the lossless archive.
21
+ - **Find specific evidence**: `thread_search` accepts `tool_name`, `path`, `task_id`, and turn-range filters and searches tool arguments/results too. Its matches contain references and offsets into the permitted content. Use `thread_find_content(ref=..., query=...)` to locate text inside one large result, then expand only the matching span.
22
+ - **Live or resumed results**: large tool results are previews with durable `r...` references. Expand or search them with the same thread tools; full output is not lost. Under context pressure, an `inventory_ref` replaces complete older tool groups and lists their arguments and result references. Retrieve only the evidence needed for the current task.
23
+ - **Resume a task**: call `thread_task_view` first. It returns recorded objective/latest previews, blocking status, modes, receipt effects with artifact IDs, and expandable archive evidence references. If `history_may_be_incomplete` is true, the scan may omit the original objective or earlier work; retrieve older evidence with task-filtered `thread_search`. Truncation flags mark omitted content; expand references rather than using abbreviated identities as full task or approval IDs.
24
+
25
+ ## What not to do
26
+
27
+ - Do not re-raise, re-narrate, or continue resolved issues from earlier rows.
28
+
29
+ ## Cross-checking memory
30
+
31
+ Memories surfaced by `memory.search` are distilled claims, not observations.
32
+ They summarize what past turns proved, but they can be stale, imprecise, or
33
+ superseded. When a recalled memory conflicts with live tool output or with a
34
+ thread turn you can read via `thread_read_range`, **the thread and the tool
35
+ win** — cite the turn you verified against. Use the memory's `source_turn`
36
+ pointer (rendered as "learned around turn N") to pull the originating turn
37
+ before acting on a recalled claim that matters.
@@ -0,0 +1,46 @@
1
+ # Pack-contributed thread redaction patterns
2
+ #
3
+ # Packs ship this file at `content/redaction_patterns.yml` to contribute
4
+ # recommended redaction patterns as a best-practice set. The host scans
5
+ # all active pack content roots at startup, merges every pack's patterns
6
+ # with the framework defaults, and compiles them into a single
7
+ # ThreadRedactionPolicy.
8
+ #
9
+ # Each entry is a raw regex string applied at read-time to thread
10
+ # `response_text` and `query` fields before they enter model context.
11
+ # Matched substrings are replaced with a strikethrough marker; the raw
12
+ # thread log is never mutated.
13
+ #
14
+ # Patterns should target harness-authored or model-self-reported text
15
+ # that contaminates subsequent turns — e.g. cap-fire diagnostics, stale
16
+ # budget claims, or mode-specific self-reports that the model
17
+ # pattern-matches against the current turn's lifecycle briefing.
18
+ #
19
+ # Convention: patterns are anchored on the distinctive prose of the
20
+ # message, not on variable parts like mode names or counts, so they
21
+ # generalize across modes and turn numbers. Use `[^\n]*` to extend to
22
+ # end-of-line rather than `.*` (which would eat across line breaks and
23
+ # redact unrelated content).
24
+
25
+ patterns:
26
+ # Model-authored: "I've hit my iteration budget in <mode> (...)."
27
+ - "I've hit my iteration budget[^\\n]*"
28
+
29
+ # Model-authored: "I've hit my iteration budget for this turn. ..."
30
+ - "I've hit my iteration budget for this turn[^\\n]*"
31
+
32
+ # Harness-fabricated capability-cap yield: "I've reached a resource
33
+ # budget for this turn. ..." — b79ba861's fallback question text. The
34
+ # 2026-10-17 turns 873/874 incident showed the claim persisting
35
+ # unredacted (none of the older phrasings match it), so the model read
36
+ # its own false exhaustion report back as ground truth.
37
+ - "I've reached a resource budget[^\\n]*"
38
+
39
+ # Harness pause diagnostic: "stuck in <mode> for N iterations (cap)"
40
+ - "stuck in \\w+ for \\d+ iterations \\(cap\\)[^\\n]*"
41
+
42
+ # Harness yield: "iteration cap N reached"
43
+ - "iteration cap \\d+ reached[^\\n]*"
44
+
45
+ # Harness yield: "turn budget exhausted"
46
+ - "turn budget exhausted[^\\n]*"
@@ -0,0 +1,17 @@
1
+ ---
2
+ mode_id: default
3
+ mode_provider_id: lisa-pack-core
4
+ mode_kind: chat
5
+ routable: true
6
+ priority: 10
7
+ description: "General conversational mode."
8
+ ---
9
+ # Default Chat
10
+
11
+ Act on the most reasonable interpretation of the request.
12
+ State the interpretation in one line, then do the work.
13
+ Seek clarification when the request is ambiguous or the intention is unclear.
14
+
15
+ When you lack a capability,
16
+ hand off: call `transition_stage` toward a mode whose tools fit the request.
17
+ Only stop to ask the operator when no mode in the catalog provides it either.
@@ -0,0 +1,43 @@
1
+ ---
2
+ mode_id: explainer
3
+ mode_provider_id: lisa-pack-core
4
+ mode_kind: chat
5
+ routable: true
6
+ command_pattern: '^(?:/)?explain(?:er)?(?:[\s,:;]+(?P<query>.+))?$'
7
+ priority: 1
8
+ fast_path_patterns:
9
+ - "\\bexplain(?:er)?\\b"
10
+ - "\\bsummarize\\b"
11
+ routing_intent_tags: [explain, expound, summarize, capture]
12
+ satisfies_intents: [explain, expound, summarize, capture]
13
+ routing_intent_keywords:
14
+ explain:
15
+ - capture
16
+ - explain
17
+ - expound
18
+ - summarize
19
+ allowed_operation_ids: [markdown_forge_document, recall_memory, synthesize_and_store_learning]
20
+ description: "Terminal last-resort mode that expounds the work and analysis done so far into a durable markdown artifact. Used by the forced-transition-before-cap path to guarantee every turn ends productively."
21
+ ---
22
+
23
+ # Explainer Mode
24
+
25
+ You are the terminal capture mode. You are reached when another mode
26
+ exhausted its budget without producing a verified write. Your only job is
27
+ to synthesize the work and analysis done so far into a durable markdown
28
+ document so the turn ends productively.
29
+
30
+ Do not perform new research. Do not explore the codebase. Do not search
31
+ the web. Use `recall_memory` to retrieve what was captured during the
32
+ prior turns, then call `markdown_forge_document` to write a synthesis
33
+ document.
34
+
35
+ The document must include:
36
+
37
+ - A header naming the workflow and the modes that ran.
38
+ - A summary of what was accomplished.
39
+ - The key findings or analysis produced.
40
+ - Any open items or next steps the prior mode did not complete.
41
+
42
+ When `markdown_forge_document` succeeds, the turn is complete. Do not
43
+ call `transition_stage` or `yield` — this is a terminal stage.
@@ -0,0 +1,41 @@
1
+ ---
2
+ mode_id: planner
3
+ mode_provider_id: lisa-pack-core
4
+ mode_kind: action
5
+ routable: true
6
+ priority: 11
7
+ fast_path_patterns:
8
+ - "\\bplan(?:ner)?\\b"
9
+ - "\\bprioritize\\b"
10
+ - "\\bmulti.?step\\b"
11
+ routing_intent_tags: [plan]
12
+ satisfies_intents: [plan]
13
+ routing_intent_keywords:
14
+ plan:
15
+ - plan
16
+ - prioritize
17
+ - workflow
18
+ - multi-step
19
+ - orchestrate
20
+ entry_operation_ids: [plan_workflow]
21
+ entry_op_kind: routing
22
+ command_pattern: '^(?:/)?plan(?:er)?(?:[\s,:;]+(?P<query>.+))?$'
23
+ needs_full_mode_catalog: true
24
+ description: "Orchestrates tasks, breaks down intent, uses fork to dispatch parallel sub-tasks."
25
+ ---
26
+
27
+ # Planner Mode
28
+
29
+ Analyze the user's request and produce a visible multi-step plan.
30
+
31
+ Call `plan_workflow` to produce the plan. The operation returns a `steps`
32
+ array (one entry per detected step, in execution order) and a
33
+ `target_stage_id` (the first step's `mode_id`). The kernel renders the
34
+ plan as a markdown block shown to the user, persists it as a
35
+ `WorkflowInstance`, and drives the handoff itself — do **not** call
36
+ `transition_stage`; the framework hands off, fans out, or chains the
37
+ next step automatically from the plan result.
38
+
39
+ When `plan_workflow` returns an empty `target_stage_id` and no `steps`
40
+ (no routable mode matched, no model wiring, or a parse failure), respond
41
+ to the user directly.
@@ -0,0 +1 @@
1
+ """Bundled ``lisa-pack-core`` operations."""
@@ -0,0 +1 @@
1
+ """Delegation operations shipped with ``lisa-pack-core``."""
@@ -0,0 +1,61 @@
1
+ # Delegate operation instruction
2
+
3
+ ## Purpose
4
+
5
+ Delegate a sequential sub-turn to another mode. The parent turn stays
6
+ alive, waits for the child's structured result, and can use the child's
7
+ output to make further decisions. This is the sequential counterpart to
8
+ the `fork` operation (which fans out in parallel and returns all
9
+ children's results to the parent for synthesis).
10
+
11
+ ## When to call
12
+
13
+ Call `delegate` when:
14
+
15
+ - You need another mode's capability and want to use its result in the
16
+ same turn (e.g. delegate to `search` for evidence, then synthesize).
17
+ - You want to build a multi-step agent workflow where each step's output
18
+ feeds the next.
19
+ - You need recursive delegation (a delegate child can itself delegate,
20
+ bounded by `DELEGATE_MAX_DEPTH`).
21
+
22
+ Do NOT call `delegate` when:
23
+
24
+ - You just need to hand off to another mode and end your turn. Use
25
+ `transition_stage` for that.
26
+ - You need parallel fan-out to multiple modes. Call the `fork` operation
27
+ for that.
28
+
29
+ ## Available Modes
30
+
31
+ The target `target_mode_id` must be a routable mode from the installed
32
+ catalog. Non-routable modes cannot be delegated to.
33
+
34
+ ## Parameters
35
+
36
+ - `target_mode_id` (required): The routable mode to delegate to.
37
+ - `query` (required): The scoped input text for the child. This is the
38
+ child's user query — not your full query. Scope it tightly.
39
+ - `stage_id` (optional): A stage within the target mode. Defaults to the
40
+ mode's start stage.
41
+ - `max_tool_iterations` (optional): Per-child iteration cap. 0 means use
42
+ the kernel default.
43
+ - `model_alias` (optional): Model override for the child. Empty means
44
+ use the target mode's resolved model.
45
+
46
+ ## Result
47
+
48
+ The operation returns a `DelegationResult` with:
49
+
50
+ - `status`: one of `ok`, `no_tools`, `timeout`, `error`, `cancelled`,
51
+ `interrupted`, `model_yield`, `max_depth`.
52
+ - `output_text`: the child's response text.
53
+ - `tool_call_count` and `called_tool_names`: what the child actually did.
54
+ - `error_message`: present when `status` is not `ok`.
55
+ - `yield_action`: present when the child yielded (asked a question).
56
+
57
+ Inspect `status` before acting on the result. `no_tools` means the
58
+ child completed without invoking its action tools — its reported success
59
+ is unverified. `interrupted` means the child hit its iteration cap before
60
+ finishing. `max_depth` means the delegation chain exceeded the recursion
61
+ limit.
@@ -0,0 +1,198 @@
1
+ """Delegate operation — sequential sub-turn delegation to another mode.
2
+
3
+ A thin pack-level handler that reads authored content, resolves the target
4
+ mode from the installed catalog, builds a :class:`DelegationSpec` from
5
+ LLM-supplied parameters, calls the injected ``ctx.host.delegate``, and
6
+ returns the :class:`DelegationResult` as the operation's structured
7
+ output.
8
+
9
+ The handler owns no routing or framework state. The kernel wires
10
+ ``delegate_fn`` into the :class:`OperationHost` (``ctx.host``) at turn
11
+ start; the default implementation (``_default_delegate_fn``) runs
12
+ ``run_turn`` recursively as a ``sub_turn_kind="delegate"`` child. A host
13
+ may override ``TurnHooks.delegate_fn`` to inject a custom delegation
14
+ backend without touching this handler.
15
+
16
+ Per the pack-content rule, the model-facing instruction prose lives in
17
+ authored Markdown (``operations/delegation/delegate.md``) loaded through
18
+ ``ctx.host.read_authored_text``. This module contains only path
19
+ constants, the structured ``Parameters`` model, and substitution glue.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import logging
25
+
26
+ from kernel.delegation import DelegationResult, DelegationSpec
27
+ from kernel.ipsar_fsm import get_workflow_catalog
28
+ from kernel.models import OperationExecutionContext, OperationResult
29
+ from pydantic import BaseModel, Field, ValidationError
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ __all__ = ["Parameters", "execute"]
34
+
35
+ _DELEGATE_INSTRUCTION_PATH = "operations/delegation/delegate.md"
36
+
37
+
38
+ class Parameters(BaseModel):
39
+ """User-supplied inputs for the delegate operation."""
40
+
41
+ target_mode_id: str = Field(
42
+ ...,
43
+ description=(
44
+ "The routable mode_id to delegate to. Must be one of the "
45
+ "Available Modes listed in the operation instruction."
46
+ ),
47
+ )
48
+ query: str = Field(
49
+ ...,
50
+ description=(
51
+ "The scoped input text for the delegated child. This is the "
52
+ "child turn's user query — not the parent's full query."
53
+ ),
54
+ )
55
+ stage_id: str = Field(
56
+ default="",
57
+ description=(
58
+ "Optional stage_id for the target mode. Defaults to the "
59
+ "mode's start_stage_id when empty."
60
+ ),
61
+ )
62
+ max_tool_iterations: int = Field(
63
+ default=0,
64
+ description=(
65
+ "Optional per-child iteration cap override. 0 means use the "
66
+ "kernel default (DELEGATE_CHILD_MAX_TOOL_ITERATIONS)."
67
+ ),
68
+ ge=0,
69
+ )
70
+ model_alias: str = Field(
71
+ default="",
72
+ description=(
73
+ "Optional model alias override for the child turn. Empty "
74
+ "means use the target mode's resolved model."
75
+ ),
76
+ )
77
+
78
+
79
+ def _resolve_parameters(ctx: OperationExecutionContext) -> Parameters:
80
+ """Build a :class:`Parameters` from the operation context.
81
+
82
+ Validates the LLM-supplied ``ctx.params`` dict against the schema. A
83
+ ``ValidationError`` surfaces a clear error to the model via the
84
+ returned ``OperationResult`` rather than crashing the turn.
85
+ """
86
+ raw = ctx.params or {}
87
+ # Allow the caller to pass the query via ctx.query when the operation
88
+ # is invoked as an entry operation (no explicit ``query`` param).
89
+ if not raw.get("query") and ctx.query:
90
+ raw = {**raw, "query": ctx.query}
91
+ return Parameters.model_validate(raw)
92
+
93
+
94
+ def _render_result_text(result: DelegationResult) -> str:
95
+ """Render a :class:`DelegationResult` as a human-readable text block.
96
+
97
+ The parent mode reads this from ``output_text``. The structured
98
+ payload (including ``status``, ``tool_call_count``, and
99
+ ``called_tool_names``) is in ``output_json``.
100
+ """
101
+ lines: list[str] = [
102
+ f"## Delegation to `{result.target_mode_id}`",
103
+ "",
104
+ f"- **Status**: `{result.status}`",
105
+ f"- **Duration**: {result.duration_ms} ms",
106
+ f"- **Tool calls**: {result.tool_call_count}",
107
+ ]
108
+ if result.called_tool_names:
109
+ lines.append(f"- **Tools called**: {', '.join(result.called_tool_names)}")
110
+ if result.error_message:
111
+ lines.append("")
112
+ lines.append(f"**Error**: {result.error_message}")
113
+ if result.output_text:
114
+ lines.append("")
115
+ lines.append("### Child output")
116
+ lines.append("")
117
+ lines.append(result.output_text)
118
+ return "\n".join(lines)
119
+
120
+
121
+ async def execute(ctx: OperationExecutionContext) -> OperationResult:
122
+ """Delegate a sub-turn to another mode and return the structured result.
123
+
124
+ Builds a :class:`DelegationSpec` from the validated parameters, calls
125
+ the injected ``ctx.host.delegate``, and returns the
126
+ :class:`DelegationResult` as both ``output_text`` (rendered) and
127
+ ``output_json`` (structured).
128
+
129
+ Fails loudly when ``delegate`` is not wired on the host (the host has
130
+ not enabled delegation) or the target mode is not in the installed
131
+ catalog — per the pack-content rule, no silent fallback.
132
+ """
133
+ try:
134
+ params = _resolve_parameters(ctx)
135
+ except ValidationError as exc:
136
+ return OperationResult(
137
+ output_text=f"delegate parameter validation failed: {exc}",
138
+ output_json={"status": "error", "error_message": str(exc)},
139
+ )
140
+
141
+ # Resolve the target mode against the live catalog so the operation
142
+ # fails loudly on an unknown mode_id rather than handing the kernel
143
+ # an invalid target.
144
+ catalog = get_workflow_catalog()
145
+ target_config = catalog.get(params.target_mode_id)
146
+ if target_config is None:
147
+ return OperationResult(
148
+ output_text=(
149
+ f"delegate target mode '{params.target_mode_id}' is not in "
150
+ "the installed catalog; cannot delegate."
151
+ ),
152
+ output_json={
153
+ "target_mode_id": params.target_mode_id,
154
+ "status": "error",
155
+ "error_message": f"unknown target mode: {params.target_mode_id}",
156
+ },
157
+ )
158
+
159
+ host = getattr(ctx, "host", None)
160
+ if host is None or getattr(host, "delegate", None) is None:
161
+ return OperationResult(
162
+ output_text=(
163
+ "ctx.host.delegate is not wired; the host has not enabled "
164
+ "delegation. Cannot delegate."
165
+ ),
166
+ output_json={
167
+ "target_mode_id": params.target_mode_id,
168
+ "status": "error",
169
+ "error_message": "host.delegate not wired",
170
+ },
171
+ )
172
+
173
+ spec = DelegationSpec(
174
+ target_mode_id=params.target_mode_id,
175
+ query=params.query,
176
+ stage_id=params.stage_id,
177
+ max_tool_iterations=params.max_tool_iterations,
178
+ model_alias=params.model_alias,
179
+ )
180
+
181
+ result = await host.delegate(spec)
182
+ if not isinstance(result, DelegationResult):
183
+ return OperationResult(
184
+ output_text=(
185
+ "runtime.delegate returned an unexpected result type "
186
+ f"({type(result).__name__}); expected DelegationResult."
187
+ ),
188
+ output_json={
189
+ "target_mode_id": params.target_mode_id,
190
+ "status": "error",
191
+ "error_message": f"unexpected result type: {type(result).__name__}",
192
+ },
193
+ )
194
+
195
+ return OperationResult(
196
+ output_text=_render_result_text(result),
197
+ output_json=result.model_dump(mode="json"),
198
+ )
@@ -0,0 +1,14 @@
1
+ operation_id: delegate
2
+ description: >
3
+ Delegate a sequential sub-turn to another mode, wait for its structured
4
+ result, and return it to the parent. Unlike fork (parallel fan-out),
5
+ delegate keeps the parent turn alive so it can use the child's output
6
+ and make further decisions. The child runs as a sub_turn_kind="delegate"
7
+ turn with its own budget and action-tool validation.
8
+ pack_id: lisa-pack-core
9
+ effect: read
10
+ execution_kind: runtime
11
+ timeout_seconds: 300
12
+ max_calls_per_turn: 5
13
+ preferred_model_id: default
14
+ exportable: true