induscode 0.1.0__py3-none-any.whl
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.
- induscode/__init__.py +56 -0
- induscode/addons/__init__.py +176 -0
- induscode/addons/contract.py +923 -0
- induscode/addons/dispatch/__init__.py +43 -0
- induscode/addons/dispatch/event_dispatcher.py +348 -0
- induscode/addons/dispatch/tool_interceptor.py +349 -0
- induscode/addons/host.py +469 -0
- induscode/addons/loader.py +314 -0
- induscode/addons/manifest.py +232 -0
- induscode/addons/surface.py +199 -0
- induscode/boot/__init__.py +108 -0
- induscode/boot/auth_vault.py +323 -0
- induscode/boot/boot.py +210 -0
- induscode/boot/contract.py +223 -0
- induscode/boot/invocation.py +117 -0
- induscode/boot/runners/__init__.py +42 -0
- induscode/boot/runners/link_runner.py +82 -0
- induscode/boot/runners/oneshot_runner.py +85 -0
- induscode/boot/runners/registry.py +46 -0
- induscode/boot/runners/repl_runner.py +340 -0
- induscode/boot/runners/session.py +549 -0
- induscode/boot/stages.py +198 -0
- induscode/boot/upgrade/__init__.py +36 -0
- induscode/boot/upgrade/apply.py +125 -0
- induscode/boot/upgrade/upgrades.py +136 -0
- induscode/briefing/__init__.py +115 -0
- induscode/briefing/compose.py +414 -0
- induscode/briefing/contract.py +528 -0
- induscode/briefing/macros.py +721 -0
- induscode/briefing/skills.py +417 -0
- induscode/capability_deck/__init__.py +233 -0
- induscode/capability_deck/bridge_ledger/__init__.py +66 -0
- induscode/capability_deck/bridge_ledger/key.py +181 -0
- induscode/capability_deck/bridge_ledger/ledger.py +276 -0
- induscode/capability_deck/bridge_ledger/network.py +336 -0
- induscode/capability_deck/builtin_bridge.py +358 -0
- induscode/capability_deck/cards/__init__.py +116 -0
- induscode/capability_deck/cards/bg_process.py +482 -0
- induscode/capability_deck/cards/memory.py +226 -0
- induscode/capability_deck/cards/saas.py +280 -0
- induscode/capability_deck/cards/task.py +256 -0
- induscode/capability_deck/cards/todo.py +312 -0
- induscode/capability_deck/contract.py +450 -0
- induscode/capability_deck/manifest.py +126 -0
- induscode/capability_deck/provision.py +217 -0
- induscode/channels/__init__.py +146 -0
- induscode/channels/contract.py +585 -0
- induscode/channels/framer.py +132 -0
- induscode/channels/link/__init__.py +50 -0
- induscode/channels/link/dialog.py +246 -0
- induscode/channels/link/driver.py +308 -0
- induscode/channels/link/server.py +217 -0
- induscode/channels/oneshot.py +178 -0
- induscode/channels/ops.py +140 -0
- induscode/channels/session_ops.py +172 -0
- induscode/conductor/__init__.py +240 -0
- induscode/conductor/catalog.py +309 -0
- induscode/conductor/conductor.py +1084 -0
- induscode/conductor/contract.py +1035 -0
- induscode/conductor/matcher.py +291 -0
- induscode/conductor/serialize.py +575 -0
- induscode/conductor/signal_hub.py +382 -0
- induscode/conductor/skill_parse.py +294 -0
- induscode/conductor/transcript_store.py +449 -0
- induscode/console/__init__.py +236 -0
- induscode/console/app.py +1677 -0
- induscode/console/components/__init__.py +62 -0
- induscode/console/components/banner.py +499 -0
- induscode/console/components/banner_sweep.py +188 -0
- induscode/console/components/emblem.py +181 -0
- induscode/console/components/status_bar.py +102 -0
- induscode/console/contract.py +836 -0
- induscode/console/input/__init__.py +107 -0
- induscode/console/input/chord.py +197 -0
- induscode/console/input/dir_reader.py +113 -0
- induscode/console/input/intents.py +258 -0
- induscode/console/input/providers.py +469 -0
- induscode/console/mount.py +137 -0
- induscode/console/overlays/__init__.py +94 -0
- induscode/console/overlays/auth.py +503 -0
- induscode/console/overlays/pickers.py +526 -0
- induscode/console/overlays/router.py +129 -0
- induscode/console/overlays/sessions.py +232 -0
- induscode/console/reducer.py +145 -0
- induscode/console/resume_picker.py +156 -0
- induscode/console/slash_commands/__init__.py +78 -0
- induscode/console/slash_commands/builtins.py +254 -0
- induscode/console/slash_commands/dynamic.py +217 -0
- induscode/console/slash_commands/integrations.py +949 -0
- induscode/console/slash_commands/transcript.py +404 -0
- induscode/console/slash_commands/workbench.py +430 -0
- induscode/console/startup.py +434 -0
- induscode/console/theme/__init__.py +44 -0
- induscode/console/theme/adapter.py +168 -0
- induscode/console/theme/palette.py +128 -0
- induscode/console/theme/resolve.py +123 -0
- induscode/console/theme/tokens.py +185 -0
- induscode/console_slash/__init__.py +111 -0
- induscode/console_slash/contract.py +185 -0
- induscode/console_slash/registry.py +140 -0
- induscode/console_slash/resolve.py +194 -0
- induscode/console_slash/shared.py +172 -0
- induscode/entry.py +108 -0
- induscode/insight/__init__.py +153 -0
- induscode/insight/collector.py +73 -0
- induscode/insight/replay.py +305 -0
- induscode/insight/wrapper.py +1115 -0
- induscode/kit/__init__.py +82 -0
- induscode/kit/clipboard_image.py +215 -0
- induscode/kit/external_editor.py +120 -0
- induscode/kit/image.py +188 -0
- induscode/kit/shell.py +89 -0
- induscode/kit/tool_fetch.py +288 -0
- induscode/launch/__init__.py +224 -0
- induscode/launch/catalog.py +310 -0
- induscode/launch/contract.py +569 -0
- induscode/launch/credentials.py +852 -0
- induscode/launch/invocation/__init__.py +39 -0
- induscode/launch/invocation/attachments.py +281 -0
- induscode/launch/invocation/flags.py +210 -0
- induscode/launch/invocation/read.py +369 -0
- induscode/launch/invocation/usage.py +110 -0
- induscode/launch/oauth.py +808 -0
- induscode/launch/packages.py +299 -0
- induscode/launch/pickers.py +291 -0
- induscode/py.typed +0 -0
- induscode/runtime_bridge/__init__.py +166 -0
- induscode/runtime_bridge/bridges/__init__.py +66 -0
- induscode/runtime_bridge/bridges/_drive.py +268 -0
- induscode/runtime_bridge/bridges/builtins.py +177 -0
- induscode/runtime_bridge/bridges/claude_cli.py +198 -0
- induscode/runtime_bridge/bridges/codex_cli.py +203 -0
- induscode/runtime_bridge/bridges/indusagi_cli.py +217 -0
- induscode/runtime_bridge/broker.py +397 -0
- induscode/runtime_bridge/contract.py +734 -0
- induscode/runtime_bridge/sink.py +351 -0
- induscode/sessions/__init__.py +25 -0
- induscode/sessions/contract.py +119 -0
- induscode/sessions/library.py +350 -0
- induscode/settings/__init__.py +47 -0
- induscode/settings/contract.py +313 -0
- induscode/settings/manager.py +268 -0
- induscode/transcript_export/__init__.py +109 -0
- induscode/transcript_export/contract.py +522 -0
- induscode/transcript_export/publish.py +455 -0
- induscode/transcript_export/sgr.py +566 -0
- induscode/transcript_export/template.py +319 -0
- induscode/transcript_export/theme_bridge.py +325 -0
- induscode/window_budget/__init__.py +76 -0
- induscode/window_budget/budget/__init__.py +26 -0
- induscode/window_budget/budget/estimate.py +273 -0
- induscode/window_budget/budget/gate.py +60 -0
- induscode/window_budget/budget/slice.py +145 -0
- induscode/window_budget/condenser.py +170 -0
- induscode/window_budget/contract.py +329 -0
- induscode/window_budget/summarize/__init__.py +33 -0
- induscode/window_budget/summarize/condense.py +212 -0
- induscode/window_budget/summarize/prompt.py +241 -0
- induscode/workspace/__init__.py +30 -0
- induscode/workspace/brand.py +96 -0
- induscode/workspace/locator.py +269 -0
- induscode-0.1.0.dist-info/METADATA +97 -0
- induscode-0.1.0.dist-info/RECORD +167 -0
- induscode-0.1.0.dist-info/WHEEL +4 -0
- induscode-0.1.0.dist-info/entry_points.txt +3 -0
- induscode-0.1.0.dist-info/licenses/CREDITS.md +22 -0
- induscode-0.1.0.dist-info/licenses/NOTICE +7 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Task capability — delegate a self-contained piece of work to a sub-agent.
|
|
2
|
+
|
|
3
|
+
App-novel wiring. The ``task`` tool lets the primary agent hand off a bounded
|
|
4
|
+
objective (e.g. "find every call site of ``foo`` and summarize them") to a
|
|
5
|
+
fresh sub-agent that runs its own tool loop and reports back a single result,
|
|
6
|
+
keeping the parent's context window clean.
|
|
7
|
+
|
|
8
|
+
The actual sub-agent runner is NOT owned by this card — it is a framework /
|
|
9
|
+
swarm concern injected through :attr:`DeckContext.framework` under the
|
|
10
|
+
:data:`DELEGATE_HANDLE_KEY` key. When that handle is present the capability
|
|
11
|
+
delegates to it; when it is absent (tests, headless tooling, a host that has
|
|
12
|
+
not wired the swarm) the capability degrades gracefully to a clearly-typed
|
|
13
|
+
stub that reports delegation is unavailable rather than throwing. Either way
|
|
14
|
+
the card builds a valid :data:`Capability` and the deck assembles and runs.
|
|
15
|
+
|
|
16
|
+
Port note: the TypeBox schema becomes a dict-literal JSON Schema, and a
|
|
17
|
+
defensive runtime guard replaces the compile-time requirement on
|
|
18
|
+
``objective``.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from collections.abc import Awaitable, Mapping, Sequence
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from typing import Literal, Protocol
|
|
26
|
+
|
|
27
|
+
from indusagi.agent import AgentToolResult
|
|
28
|
+
from indusagi.ai import TextContent
|
|
29
|
+
|
|
30
|
+
from ..contract import Capability, CapabilityCard, DeckContext, Schema, capability_id
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"DELEGATE_HANDLE_KEY",
|
|
34
|
+
"DelegateRequest",
|
|
35
|
+
"DelegateResult",
|
|
36
|
+
"DelegateRunner",
|
|
37
|
+
"TaskDetails",
|
|
38
|
+
"build_task_capability",
|
|
39
|
+
"task_card",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Delegate handle (injected via DeckContext.framework)
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
#: Key under which a host wires a live delegate runner into the deck context.
|
|
48
|
+
DELEGATE_HANDLE_KEY = "delegate"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
52
|
+
class DelegateRequest:
|
|
53
|
+
"""A single delegated objective handed to the sub-agent runner."""
|
|
54
|
+
|
|
55
|
+
# The self-contained objective the sub-agent should accomplish.
|
|
56
|
+
objective: str
|
|
57
|
+
# Optional named agent profile to run as (e.g. "explorer", "reviewer").
|
|
58
|
+
agent: str | None = None
|
|
59
|
+
# Optional extra context the parent wants the sub-agent to start with.
|
|
60
|
+
context: str | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
64
|
+
class DelegateResult:
|
|
65
|
+
"""The single result a sub-agent reports back to the parent."""
|
|
66
|
+
|
|
67
|
+
# Whether the sub-agent considers the objective met.
|
|
68
|
+
ok: bool
|
|
69
|
+
# The sub-agent's final report, surfaced to the parent agent verbatim.
|
|
70
|
+
report: str
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class DelegateRunner(Protocol):
|
|
74
|
+
"""The contract a host's sub-agent runner must satisfy to be wired in.
|
|
75
|
+
|
|
76
|
+
Intentionally minimal: the deck only needs a way to run one objective
|
|
77
|
+
under cancellation and get one report back. How the runner spawns the
|
|
78
|
+
sub-agent (in-process loop, worktree, separate process) is entirely the
|
|
79
|
+
host's concern. A runner *may* also expose a ``list_agents()`` returning
|
|
80
|
+
the named profiles it offers; the tool description surfaces them when
|
|
81
|
+
present (read via ``getattr``, the Python stand-in for the TS optional
|
|
82
|
+
method).
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def run(
|
|
86
|
+
self, request: DelegateRequest, signal: object | None = None
|
|
87
|
+
) -> Awaitable[DelegateResult]:
|
|
88
|
+
"""Run one delegated objective and resolve with the sub-agent's
|
|
89
|
+
report."""
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _read_delegate_runner(ctx: DeckContext) -> DelegateRunner | None:
|
|
94
|
+
handle = (ctx.framework or {}).get(DELEGATE_HANDLE_KEY)
|
|
95
|
+
if handle is not None and callable(getattr(handle, "run", None)):
|
|
96
|
+
return handle # type: ignore[return-value] — structural check above
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _list_agents(runner: DelegateRunner | None) -> Sequence[str]:
|
|
101
|
+
lister = getattr(runner, "list_agents", None)
|
|
102
|
+
if callable(lister):
|
|
103
|
+
try:
|
|
104
|
+
return tuple(lister())
|
|
105
|
+
except Exception:
|
|
106
|
+
return ()
|
|
107
|
+
return ()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
# Parameters (dict-literal JSON Schema)
|
|
112
|
+
# ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
_TASK_PARAMS: Schema = {
|
|
115
|
+
"type": "object",
|
|
116
|
+
"properties": {
|
|
117
|
+
"objective": {
|
|
118
|
+
"type": "string",
|
|
119
|
+
"description": (
|
|
120
|
+
"A complete, self-contained statement of what the sub-agent should "
|
|
121
|
+
"accomplish. Include everything it needs; it does not share your "
|
|
122
|
+
"conversation history."
|
|
123
|
+
),
|
|
124
|
+
},
|
|
125
|
+
"agent": {
|
|
126
|
+
"type": "string",
|
|
127
|
+
"description": (
|
|
128
|
+
"Optional named sub-agent profile to run as. Omit to use the "
|
|
129
|
+
"default profile."
|
|
130
|
+
),
|
|
131
|
+
},
|
|
132
|
+
"context": {
|
|
133
|
+
"type": "string",
|
|
134
|
+
"description": "Optional extra background the sub-agent should start with.",
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
"required": ["objective"],
|
|
138
|
+
"additionalProperties": False,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
143
|
+
class TaskDetails:
|
|
144
|
+
"""Structured detail returned alongside the model-facing content."""
|
|
145
|
+
|
|
146
|
+
# True when a delegate runner handled the objective.
|
|
147
|
+
delegated: bool
|
|
148
|
+
# True when the sub-agent (or stub) considers the objective met.
|
|
149
|
+
ok: bool
|
|
150
|
+
# The agent profile that ran, if one was named.
|
|
151
|
+
agent: str | None = None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _base_description(agents: Sequence[str]) -> str:
|
|
155
|
+
roster = f" Available sub-agent profiles: {', '.join(agents)}." if agents else ""
|
|
156
|
+
return (
|
|
157
|
+
"Delegate a focused, self-contained piece of work to a sub-agent that runs its own "
|
|
158
|
+
"tool loop and returns a single report. Use this to keep your own context clean when a "
|
|
159
|
+
"task is well-scoped — searching a large codebase, drafting a file, or investigating a "
|
|
160
|
+
"question end-to-end. Give a complete `objective`; the sub-agent does not see your "
|
|
161
|
+
"conversation." + roster
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
_STUB_NOTE = (
|
|
166
|
+
"Sub-agent delegation is not wired in this environment, so the objective was not run. "
|
|
167
|
+
"Wire a DelegateRunner into the deck context to enable it, or perform the work inline "
|
|
168
|
+
"with the other tools."
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _field(params: object, key: str) -> object:
|
|
173
|
+
if isinstance(params, Mapping):
|
|
174
|
+
return params.get(key)
|
|
175
|
+
return getattr(params, key, None)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
# Capability builder
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class _TaskCapability:
|
|
184
|
+
"""The live task/delegate capability — structurally an ``AgentTool``."""
|
|
185
|
+
|
|
186
|
+
name = "task"
|
|
187
|
+
label = "Delegate task"
|
|
188
|
+
parameters: Schema = _TASK_PARAMS
|
|
189
|
+
|
|
190
|
+
def __init__(self, runner: DelegateRunner | None) -> None:
|
|
191
|
+
self._runner = runner
|
|
192
|
+
self.description = _base_description(_list_agents(runner))
|
|
193
|
+
|
|
194
|
+
async def execute(
|
|
195
|
+
self,
|
|
196
|
+
tool_call_id: str,
|
|
197
|
+
params: object,
|
|
198
|
+
signal: object = None,
|
|
199
|
+
on_update: object = None,
|
|
200
|
+
) -> AgentToolResult:
|
|
201
|
+
del tool_call_id, on_update
|
|
202
|
+
agent_raw = _field(params, "agent")
|
|
203
|
+
agent = agent_raw if isinstance(agent_raw, str) else None
|
|
204
|
+
if self._runner is None:
|
|
205
|
+
return AgentToolResult(
|
|
206
|
+
content=(TextContent(text=_STUB_NOTE),),
|
|
207
|
+
details=TaskDetails(delegated=False, ok=False, agent=agent),
|
|
208
|
+
isError=True,
|
|
209
|
+
)
|
|
210
|
+
objective = _field(params, "objective")
|
|
211
|
+
if not isinstance(objective, str) or objective == "":
|
|
212
|
+
return AgentToolResult(
|
|
213
|
+
content=(
|
|
214
|
+
TextContent(text="`objective` is required to delegate a task."),
|
|
215
|
+
),
|
|
216
|
+
details=TaskDetails(delegated=False, ok=False, agent=agent),
|
|
217
|
+
isError=True,
|
|
218
|
+
)
|
|
219
|
+
context_raw = _field(params, "context")
|
|
220
|
+
context = context_raw if isinstance(context_raw, str) else None
|
|
221
|
+
result = await self._runner.run(
|
|
222
|
+
DelegateRequest(objective=objective, agent=agent, context=context),
|
|
223
|
+
signal,
|
|
224
|
+
)
|
|
225
|
+
return AgentToolResult(
|
|
226
|
+
content=(TextContent(text=result.report),),
|
|
227
|
+
details=TaskDetails(delegated=True, ok=result.ok, agent=agent),
|
|
228
|
+
isError=not result.ok,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def build_task_capability(ctx: DeckContext) -> _TaskCapability:
|
|
233
|
+
"""Build the task/delegate capability.
|
|
234
|
+
|
|
235
|
+
If a :class:`DelegateRunner` is present on the context it is bound and the
|
|
236
|
+
tool truly delegates; otherwise the tool builds anyway and returns a
|
|
237
|
+
typed, non-throwing stub result so the deck stays assemblable in every
|
|
238
|
+
environment.
|
|
239
|
+
|
|
240
|
+
:param ctx: the deck context; an optional delegate runner is read from
|
|
241
|
+
``ctx.framework[DELEGATE_HANDLE_KEY]``
|
|
242
|
+
"""
|
|
243
|
+
return _TaskCapability(_read_delegate_runner(ctx))
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _build(ctx: DeckContext) -> Capability:
|
|
247
|
+
return build_task_capability(ctx)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
#: Catalog row for the task/delegate capability.
|
|
251
|
+
task_card = CapabilityCard(
|
|
252
|
+
id=capability_id("task"),
|
|
253
|
+
title="Delegate task",
|
|
254
|
+
summary="Hand a self-contained objective to a sub-agent and receive one report back.",
|
|
255
|
+
build=_build,
|
|
256
|
+
)
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""Todo capability — an in-memory checklist the agent sets/reads during a run.
|
|
2
|
+
|
|
3
|
+
App-novel and framework-agnostic: the store is a plain in-process tuple of
|
|
4
|
+
items, owned by the card and closed over by the built capability. No file
|
|
5
|
+
persistence, no session-branch reconstruction, no framework store — just a
|
|
6
|
+
mutable list the agent rewrites wholesale (the model is expected to send the
|
|
7
|
+
complete desired list on every ``set``, the same convention coding agents use
|
|
8
|
+
so the plan stays a single coherent snapshot rather than a diff stream).
|
|
9
|
+
|
|
10
|
+
Two operations are folded into one tool keyed by an ``action`` discriminant:
|
|
11
|
+
|
|
12
|
+
- ``read`` — return the current checklist as model-facing prose.
|
|
13
|
+
- ``set`` — replace the checklist with the supplied items (the authoritative
|
|
14
|
+
new plan), then echo it back.
|
|
15
|
+
|
|
16
|
+
The card produces a :data:`Capability` (the framework ``AgentTool`` shape) so
|
|
17
|
+
the conductor consumes it verbatim as one of ``options.tools``.
|
|
18
|
+
|
|
19
|
+
Port note — TypeBox → dict-literal JSON Schema + runtime guards
|
|
20
|
+
---------------------------------------------------------------
|
|
21
|
+
The TS card's TypeBox schema becomes a plain JSON-schema mapping (the
|
|
22
|
+
framework's ``parameters: Mapping`` convention), and the compile-time
|
|
23
|
+
``Static`` typing it bought is replaced by defensive runtime guards: a
|
|
24
|
+
malformed ``action`` or ``items`` yields an ``isError`` result instead of an
|
|
25
|
+
exception, because nothing upstream is guaranteed to have validated the
|
|
26
|
+
model's arguments.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from collections.abc import Mapping, Sequence
|
|
32
|
+
from dataclasses import dataclass
|
|
33
|
+
from typing import Literal, TypeAlias
|
|
34
|
+
|
|
35
|
+
from indusagi.agent import AgentToolResult
|
|
36
|
+
from indusagi.ai import TextContent
|
|
37
|
+
|
|
38
|
+
from ..contract import Capability, CapabilityCard, DeckContext, Schema, capability_id
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"TodoDetails",
|
|
42
|
+
"TodoItem",
|
|
43
|
+
"TodoLedger",
|
|
44
|
+
"TodoState",
|
|
45
|
+
"TodoWeight",
|
|
46
|
+
"build_todo_capability",
|
|
47
|
+
"todo_card",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# Store (in-memory, card-owned)
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
#: Lifecycle state of one checklist item.
|
|
56
|
+
TodoState: TypeAlias = Literal["pending", "active", "done", "dropped"]
|
|
57
|
+
|
|
58
|
+
#: Relative importance of one checklist item.
|
|
59
|
+
TodoWeight: TypeAlias = Literal["low", "normal", "high"]
|
|
60
|
+
|
|
61
|
+
_STATES: tuple[TodoState, ...] = ("pending", "active", "done", "dropped")
|
|
62
|
+
_WEIGHTS: tuple[TodoWeight, ...] = ("low", "normal", "high")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
66
|
+
class TodoItem:
|
|
67
|
+
"""One row of the in-memory checklist."""
|
|
68
|
+
|
|
69
|
+
# The work to be done, phrased as a short imperative.
|
|
70
|
+
task: str
|
|
71
|
+
# Where the item stands right now.
|
|
72
|
+
state: TodoState
|
|
73
|
+
# How much it matters relative to the rest of the list.
|
|
74
|
+
weight: TodoWeight
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class TodoLedger:
|
|
78
|
+
"""A minimal in-process checklist store.
|
|
79
|
+
|
|
80
|
+
Deliberately tiny and synchronous: the whole list lives in one field,
|
|
81
|
+
:meth:`set` swaps it atomically, and :meth:`read` hands back an immutable
|
|
82
|
+
tuple so a caller cannot mutate the store's backing sequence. One store is
|
|
83
|
+
created per built capability, so two sessions never share a checklist.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(self) -> None:
|
|
87
|
+
self._items: tuple[TodoItem, ...] = ()
|
|
88
|
+
|
|
89
|
+
def set(self, next_items: Sequence[TodoItem]) -> tuple[TodoItem, ...]:
|
|
90
|
+
"""Replace the entire checklist with ``next_items``; returns the
|
|
91
|
+
stored snapshot."""
|
|
92
|
+
self._items = tuple(next_items)
|
|
93
|
+
return self._items
|
|
94
|
+
|
|
95
|
+
def read(self) -> tuple[TodoItem, ...]:
|
|
96
|
+
"""Return the current checklist as an immutable snapshot."""
|
|
97
|
+
return self._items
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
# Parameters (dict-literal JSON Schema)
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
_TODO_ITEM_SCHEMA: Schema = {
|
|
105
|
+
"type": "object",
|
|
106
|
+
"properties": {
|
|
107
|
+
"task": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"description": "Short imperative description of the work item.",
|
|
110
|
+
},
|
|
111
|
+
"state": {
|
|
112
|
+
"type": "string",
|
|
113
|
+
"enum": list(_STATES),
|
|
114
|
+
"description": "Lifecycle state of the item.",
|
|
115
|
+
},
|
|
116
|
+
"weight": {
|
|
117
|
+
"type": "string",
|
|
118
|
+
"enum": list(_WEIGHTS),
|
|
119
|
+
"description": "Relative importance of the item.",
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
"required": ["task", "state", "weight"],
|
|
123
|
+
"additionalProperties": False,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
_TODO_PARAMS: Schema = {
|
|
127
|
+
"type": "object",
|
|
128
|
+
"properties": {
|
|
129
|
+
"action": {
|
|
130
|
+
"type": "string",
|
|
131
|
+
"enum": ["read", "set"],
|
|
132
|
+
"description": (
|
|
133
|
+
"`read` returns the current checklist; `set` replaces it with "
|
|
134
|
+
"the items you provide."
|
|
135
|
+
),
|
|
136
|
+
},
|
|
137
|
+
"items": {
|
|
138
|
+
"type": "array",
|
|
139
|
+
"items": _TODO_ITEM_SCHEMA,
|
|
140
|
+
"description": (
|
|
141
|
+
"The complete desired checklist. Required for `set` (send the "
|
|
142
|
+
"whole list, not a delta); ignored for `read`."
|
|
143
|
+
),
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
"required": ["action"],
|
|
147
|
+
"additionalProperties": False,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
152
|
+
class TodoDetails:
|
|
153
|
+
"""Structured detail returned alongside the model-facing content."""
|
|
154
|
+
|
|
155
|
+
# The action that was performed.
|
|
156
|
+
action: Literal["read", "set"]
|
|
157
|
+
# The checklist as it stands after the call.
|
|
158
|
+
items: tuple[TodoItem, ...]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
_TODO_DESCRIPTION = (
|
|
162
|
+
'Maintain a working checklist for the current task. Call with `action:"set"` and a '
|
|
163
|
+
"complete `items` list to record or revise your plan — always send the full intended "
|
|
164
|
+
'list, since the previous one is discarded. Call with `action:"read"` to recall the '
|
|
165
|
+
"current plan. Mark items `active` while in progress and `done` when finished so the "
|
|
166
|
+
"list reflects real status; use `dropped` for work you decided to skip."
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
# Rendering
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
_STATE_GLYPH: Mapping[str, str] = {
|
|
175
|
+
"pending": "[ ]",
|
|
176
|
+
"active": "[~]",
|
|
177
|
+
"done": "[x]",
|
|
178
|
+
"dropped": "[-]",
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _render_checklist(items: Sequence[TodoItem]) -> str:
|
|
183
|
+
if not items:
|
|
184
|
+
return "The checklist is empty."
|
|
185
|
+
lines = []
|
|
186
|
+
for item in items:
|
|
187
|
+
weight = "" if item.weight == "normal" else f" ({item.weight})"
|
|
188
|
+
lines.append(f"{_STATE_GLYPH[item.state]} {item.task}{weight}")
|
|
189
|
+
return "\n".join(lines)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
# Runtime guards (the Static<typeof TodoParams> stand-in)
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _field(params: object, key: str) -> object:
|
|
198
|
+
"""Read one argument off the model's params, whether they arrived as a
|
|
199
|
+
mapping (the wire form) or an attribute-bearing object (direct callers)."""
|
|
200
|
+
if isinstance(params, Mapping):
|
|
201
|
+
return params.get(key)
|
|
202
|
+
return getattr(params, key, None)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _parse_items(raw: object) -> tuple[TodoItem, ...]:
|
|
206
|
+
"""Validate and coerce the ``items`` argument; raises :class:`ValueError`
|
|
207
|
+
on any shape the schema would have rejected."""
|
|
208
|
+
if raw is None:
|
|
209
|
+
return ()
|
|
210
|
+
if not isinstance(raw, Sequence) or isinstance(raw, (str, bytes)):
|
|
211
|
+
raise ValueError("`items` must be an array of checklist items.")
|
|
212
|
+
items: list[TodoItem] = []
|
|
213
|
+
for entry in raw:
|
|
214
|
+
if isinstance(entry, TodoItem):
|
|
215
|
+
items.append(entry)
|
|
216
|
+
continue
|
|
217
|
+
if not isinstance(entry, Mapping):
|
|
218
|
+
raise ValueError("each checklist item must be an object.")
|
|
219
|
+
task = entry.get("task")
|
|
220
|
+
if not isinstance(task, str) or task == "":
|
|
221
|
+
raise ValueError("each checklist item requires a `task` string.")
|
|
222
|
+
state = entry.get("state")
|
|
223
|
+
if state not in _STATES:
|
|
224
|
+
raise ValueError(
|
|
225
|
+
"each checklist item requires a `state` of "
|
|
226
|
+
"pending | active | done | dropped."
|
|
227
|
+
)
|
|
228
|
+
weight = entry.get("weight")
|
|
229
|
+
if weight not in _WEIGHTS:
|
|
230
|
+
raise ValueError(
|
|
231
|
+
"each checklist item requires a `weight` of low | normal | high."
|
|
232
|
+
)
|
|
233
|
+
items.append(TodoItem(task=task, state=state, weight=weight))
|
|
234
|
+
return tuple(items)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# ---------------------------------------------------------------------------
|
|
238
|
+
# Capability builder
|
|
239
|
+
# ---------------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class _TodoCapability:
|
|
243
|
+
"""The live todo capability — structurally an ``AgentTool``."""
|
|
244
|
+
|
|
245
|
+
name = "todo"
|
|
246
|
+
label = "Checklist"
|
|
247
|
+
description = _TODO_DESCRIPTION
|
|
248
|
+
parameters: Schema = _TODO_PARAMS
|
|
249
|
+
|
|
250
|
+
def __init__(self) -> None:
|
|
251
|
+
self._ledger = TodoLedger()
|
|
252
|
+
|
|
253
|
+
async def execute(
|
|
254
|
+
self,
|
|
255
|
+
tool_call_id: str,
|
|
256
|
+
params: object,
|
|
257
|
+
signal: object = None,
|
|
258
|
+
on_update: object = None,
|
|
259
|
+
) -> AgentToolResult:
|
|
260
|
+
del tool_call_id, signal, on_update
|
|
261
|
+
action = _field(params, "action")
|
|
262
|
+
if action not in ("read", "set"):
|
|
263
|
+
return AgentToolResult(
|
|
264
|
+
content=(TextContent(text='`action` must be "read" or "set".'),),
|
|
265
|
+
details=None,
|
|
266
|
+
isError=True,
|
|
267
|
+
)
|
|
268
|
+
if action == "set":
|
|
269
|
+
try:
|
|
270
|
+
items = self._ledger.set(_parse_items(_field(params, "items")))
|
|
271
|
+
except ValueError as bad:
|
|
272
|
+
return AgentToolResult(
|
|
273
|
+
content=(TextContent(text=str(bad)),),
|
|
274
|
+
details=None,
|
|
275
|
+
isError=True,
|
|
276
|
+
)
|
|
277
|
+
heading = "Checklist updated:"
|
|
278
|
+
else:
|
|
279
|
+
items = self._ledger.read()
|
|
280
|
+
heading = "Current checklist:"
|
|
281
|
+
text = f"{heading}\n{_render_checklist(items)}"
|
|
282
|
+
return AgentToolResult(
|
|
283
|
+
content=(TextContent(text=text),),
|
|
284
|
+
details=TodoDetails(action=action, items=items),
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def build_todo_capability(_ctx: DeckContext) -> _TodoCapability:
|
|
289
|
+
"""Build the todo capability, binding it to a freshly created in-memory
|
|
290
|
+
ledger.
|
|
291
|
+
|
|
292
|
+
The ledger is owned by the returned capability, so the checklist persists
|
|
293
|
+
for the life of this capability instance (i.e. the session) without any
|
|
294
|
+
external store.
|
|
295
|
+
|
|
296
|
+
:param _ctx: the deck context (unused — the todo card needs no
|
|
297
|
+
cwd/backends)
|
|
298
|
+
"""
|
|
299
|
+
return _TodoCapability()
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _build(ctx: DeckContext) -> Capability:
|
|
303
|
+
return build_todo_capability(ctx)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
#: Catalog row for the todo capability — registered in ``APP_NOVEL_CARDS``.
|
|
307
|
+
todo_card = CapabilityCard(
|
|
308
|
+
id=capability_id("todo"),
|
|
309
|
+
title="Checklist",
|
|
310
|
+
summary="Keep an in-memory checklist of the current task's steps; set or read it.",
|
|
311
|
+
build=_build,
|
|
312
|
+
)
|