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.
- lisa_pack_core-0.1.0/.gitignore +33 -0
- lisa_pack_core-0.1.0/PKG-INFO +10 -0
- lisa_pack_core-0.1.0/pyproject.toml +35 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/__init__.py +14 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/content/prompts/sub_turn/budget_exhausted.md +3 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/content/prompts/sub_turn/delegate_contract.md +8 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/content/prompts/sub_turn/fork_contract.md +8 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/content/prompts/sub_turn/midpoint_write_nudge.md +6 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/content/prompts/sub_turn/no_write_warning.md +4 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/content/prompts/system-prompt.md +9 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/content/prompts/thread_manifest_hint.md +37 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/content/redaction_patterns.yml +46 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/modes/default/_mode.md +17 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/modes/explainer/_mode.md +43 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/modes/planner/_mode.md +41 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/operations/__init__.py +1 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/operations/delegation/__init__.py +1 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/operations/delegation/delegate.md +61 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/operations/delegation/delegate.py +198 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/operations/delegation/delegate.yml +14 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/operations/delegation/fork.md +73 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/operations/delegation/fork.py +232 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/operations/delegation/fork.yml +15 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/operations/planning/__init__.py +1 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/operations/planning/plan_workflow.py +691 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/operations/planning/plan_workflow.yml +14 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/sessions/default.md +116 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/tools/__init__.py +0 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/tools/fetch_page/__init__.py +0 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/tools/fetch_page/_models.py +21 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/tools/fetch_page/fetch_page.py +251 -0
- lisa_pack_core-0.1.0/src/sdk/packs/core/tools/fetch_page/fetch_page.tool.yml +9 -0
- lisa_pack_core-0.1.0/tests/test_delegate_fn.py +186 -0
- lisa_pack_core-0.1.0/tests/test_delegate_operation.py +195 -0
- lisa_pack_core-0.1.0/tests/test_fetch_page_tool.py +112 -0
- lisa_pack_core-0.1.0/tests/test_fork_fn.py +204 -0
- lisa_pack_core-0.1.0/tests/test_fork_operation.py +381 -0
- 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,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,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
|