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,198 @@
|
|
|
1
|
+
"""``claude-cli`` bridge — drives an Anthropic-flavoured CLI that emits
|
|
2
|
+
line-delimited stream-json content blocks (port of TS
|
|
3
|
+
``src/runtime-bridge/bridges/claude-cli.ts``).
|
|
4
|
+
|
|
5
|
+
Wire dialect (each inbound ``ChildMessage.payload`` is one parsed JSON line
|
|
6
|
+
of the CLI's NDJSON stdout):
|
|
7
|
+
|
|
8
|
+
- ``{"type": "system", "subtype": "init", "session_id": …}`` — the CLI
|
|
9
|
+
announces the session id it allocated; surfaced as a ``resume`` event so
|
|
10
|
+
the broker can persist it and reattach the same CLI session later.
|
|
11
|
+
- ``{"type": "content_block_start", "content_block": {"type": …}}`` — a
|
|
12
|
+
block opens; a ``tool_use`` block carries ``{id, name, input}`` which
|
|
13
|
+
becomes a fully formed tool call immediately (the CLI batches the input).
|
|
14
|
+
- ``{"type": "content_block_delta", "delta": {"type", "text" | "thinking"}}``
|
|
15
|
+
— a ``text_delta`` / ``thinking_delta`` chunk; mapped to a ``text`` /
|
|
16
|
+
``thinking`` normalized event. ``input_json_delta`` chunks (streamed tool
|
|
17
|
+
arguments) are ignored here because the bridge emits the tool call from
|
|
18
|
+
the terminal ``content_block_stop`` snapshot.
|
|
19
|
+
- ``{"type": "message_delta", "delta": {"stop_reason": …}}`` — carries the
|
|
20
|
+
final stop reason, recorded for the eventual ``finish``.
|
|
21
|
+
- ``{"type": "message_stop"}`` / ``{"type": "result"}`` — the turn is over;
|
|
22
|
+
finish with the recorded reason.
|
|
23
|
+
- ``{"type": "error", "error": {"message": …}}`` — a runtime fault.
|
|
24
|
+
|
|
25
|
+
The opening request is the user turn forwarded as the CLI's prompt body; the
|
|
26
|
+
spec's binary/args/env are the broker's concern when it builds the
|
|
27
|
+
transport, so this bridge only formats the protocol body.
|
|
28
|
+
|
|
29
|
+
The child process is reached exclusively through the injected
|
|
30
|
+
``ChildTransport``; no ``claude`` binary is spawned here.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from collections.abc import Mapping
|
|
36
|
+
from typing import Any, Final
|
|
37
|
+
|
|
38
|
+
from induscode.runtime_bridge.contract import (
|
|
39
|
+
AssistantMessageEventStream,
|
|
40
|
+
BridgeEventSink,
|
|
41
|
+
BridgeFailure,
|
|
42
|
+
ChildMessage,
|
|
43
|
+
ChildRequest,
|
|
44
|
+
ChildTransport,
|
|
45
|
+
Context,
|
|
46
|
+
ExchangeOptions,
|
|
47
|
+
ExternalRuntimeSpec,
|
|
48
|
+
FinishReason,
|
|
49
|
+
Model,
|
|
50
|
+
ResumeEvent,
|
|
51
|
+
RuntimeAdapterId,
|
|
52
|
+
RuntimeBridge,
|
|
53
|
+
ToolCall,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
from ._drive import CONTINUE, DONE, ChildParser, ParseStep, as_record, drive_exchange, str_field
|
|
57
|
+
|
|
58
|
+
__all__ = [
|
|
59
|
+
"claude_cli_bridge",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
#: The adapter id this bridge answers to.
|
|
63
|
+
_ADAPTER: Final = "claude-cli"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _map_stop_reason(raw: str | None) -> FinishReason:
|
|
67
|
+
"""Map an Anthropic ``stop_reason`` string onto a normalized
|
|
68
|
+
:data:`~induscode.runtime_bridge.contract.FinishReason`."""
|
|
69
|
+
if raw == "max_tokens":
|
|
70
|
+
return "length"
|
|
71
|
+
if raw == "tool_use":
|
|
72
|
+
return "toolUse"
|
|
73
|
+
# "end_turn" / "stop_sequence" / anything else settles as a plain stop.
|
|
74
|
+
return "stop"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _tool_call_from_block(block: Mapping[str, Any]) -> ToolCall | None:
|
|
78
|
+
"""Extract a fully-formed ``ToolCall`` from a ``tool_use`` content block."""
|
|
79
|
+
id_ = str_field(block, "id")
|
|
80
|
+
name = str_field(block, "name")
|
|
81
|
+
if id_ is None or name is None:
|
|
82
|
+
return None
|
|
83
|
+
input_ = as_record(block.get("input")) or {}
|
|
84
|
+
return ToolCall(id=id_, name=name, arguments=input_)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _make_parser() -> ChildParser:
|
|
88
|
+
"""Parse one stream-json line into sink emissions. Records the last-seen
|
|
89
|
+
stop reason on a small per-exchange closure so ``message_stop`` can
|
|
90
|
+
finish with it."""
|
|
91
|
+
stop_reason: FinishReason = "stop"
|
|
92
|
+
|
|
93
|
+
def parse(message: ChildMessage, sink: BridgeEventSink) -> ParseStep:
|
|
94
|
+
nonlocal stop_reason
|
|
95
|
+
payload = as_record(message.payload)
|
|
96
|
+
if payload is None:
|
|
97
|
+
return CONTINUE
|
|
98
|
+
type_ = str_field(payload, "type")
|
|
99
|
+
|
|
100
|
+
match type_:
|
|
101
|
+
case "system":
|
|
102
|
+
# Session init: surface the allocated session id as a resume token.
|
|
103
|
+
if str_field(payload, "subtype") == "init":
|
|
104
|
+
session_id = str_field(payload, "session_id")
|
|
105
|
+
if session_id is not None:
|
|
106
|
+
sink.emit(ResumeEvent(resumeToken=session_id))
|
|
107
|
+
return CONTINUE
|
|
108
|
+
|
|
109
|
+
case "content_block_start":
|
|
110
|
+
block = as_record(payload.get("content_block"))
|
|
111
|
+
if block is not None and str_field(block, "type") == "tool_use":
|
|
112
|
+
call = _tool_call_from_block(block)
|
|
113
|
+
if call is not None:
|
|
114
|
+
sink.tool_call(call)
|
|
115
|
+
return CONTINUE
|
|
116
|
+
|
|
117
|
+
case "content_block_delta":
|
|
118
|
+
delta = as_record(payload.get("delta"))
|
|
119
|
+
if delta is None:
|
|
120
|
+
return CONTINUE
|
|
121
|
+
delta_type = str_field(delta, "type")
|
|
122
|
+
if delta_type == "text_delta":
|
|
123
|
+
text = str_field(delta, "text")
|
|
124
|
+
if text is not None:
|
|
125
|
+
sink.text(text)
|
|
126
|
+
elif delta_type == "thinking_delta":
|
|
127
|
+
thinking = str_field(delta, "thinking")
|
|
128
|
+
if thinking is not None:
|
|
129
|
+
sink.thinking(thinking)
|
|
130
|
+
# input_json_delta is intentionally dropped — see module header.
|
|
131
|
+
return CONTINUE
|
|
132
|
+
|
|
133
|
+
case "message_delta":
|
|
134
|
+
delta = as_record(payload.get("delta"))
|
|
135
|
+
stop_reason = _map_stop_reason(
|
|
136
|
+
str_field(delta, "stop_reason") if delta is not None else None
|
|
137
|
+
)
|
|
138
|
+
return CONTINUE
|
|
139
|
+
|
|
140
|
+
case "error":
|
|
141
|
+
error = as_record(payload.get("error"))
|
|
142
|
+
detail = (
|
|
143
|
+
str_field(error, "message") if error is not None else None
|
|
144
|
+
) or "claude-cli error"
|
|
145
|
+
sink.finish_error(BridgeFailure(message=detail, cause=payload.get("error")))
|
|
146
|
+
return DONE
|
|
147
|
+
|
|
148
|
+
case "message_stop" | "result":
|
|
149
|
+
sink.finish_success(stop_reason)
|
|
150
|
+
return DONE
|
|
151
|
+
|
|
152
|
+
case _:
|
|
153
|
+
return CONTINUE
|
|
154
|
+
|
|
155
|
+
return parse
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _opening_request(context: Context, opts: ExchangeOptions) -> ChildRequest:
|
|
159
|
+
"""Build the opening request: forward the conversation context as the CLI
|
|
160
|
+
prompt body, threading the resume token / cwd through so the transport
|
|
161
|
+
layer can attach the right CLI session."""
|
|
162
|
+
return ChildRequest(
|
|
163
|
+
body={
|
|
164
|
+
"type": "user",
|
|
165
|
+
"context": context,
|
|
166
|
+
"resume": opts.resume,
|
|
167
|
+
"cwd": opts.cwd,
|
|
168
|
+
"sessionId": opts.sessionId,
|
|
169
|
+
}
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class _ClaudeCliBridge:
|
|
174
|
+
"""The ``claude-cli`` :class:`RuntimeBridge`. Stateless on the contract
|
|
175
|
+
surface: it parses the injected transport's stream-json into the sink and
|
|
176
|
+
returns the framework push stream."""
|
|
177
|
+
|
|
178
|
+
adapter: RuntimeAdapterId = _ADAPTER
|
|
179
|
+
|
|
180
|
+
def run_exchange(
|
|
181
|
+
self,
|
|
182
|
+
model: Model,
|
|
183
|
+
context: Context,
|
|
184
|
+
opts: ExchangeOptions,
|
|
185
|
+
transport: ChildTransport,
|
|
186
|
+
) -> AssistantMessageEventStream:
|
|
187
|
+
return drive_exchange(
|
|
188
|
+
model, opts, transport, _opening_request(context, opts), _make_parser()
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def requires_credential(self, spec: ExternalRuntimeSpec) -> bool:
|
|
192
|
+
# An external CLI that owns its own login needs no key on disk; an
|
|
193
|
+
# api-key spec still resolves a credential from the vault.
|
|
194
|
+
return spec.authMode == "api-key"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
#: The shipped ``claude-cli`` bridge singleton.
|
|
198
|
+
claude_cli_bridge: RuntimeBridge = _ClaudeCliBridge()
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""``codex-cli`` bridge — drives an OpenAI-flavoured CLI emitting ``--json``
|
|
2
|
+
turn/item events (port of TS ``src/runtime-bridge/bridges/codex-cli.ts``).
|
|
3
|
+
|
|
4
|
+
Wire dialect (each inbound ``ChildMessage.payload`` is one parsed ``--json``
|
|
5
|
+
event object):
|
|
6
|
+
|
|
7
|
+
- ``{"type": "thread.started", "thread_id": …}`` — the CLI opened a thread;
|
|
8
|
+
surfaced as a ``resume`` event so the broker can persist the thread id and
|
|
9
|
+
continue it on a later exchange.
|
|
10
|
+
- ``{"type": "response.output_text.delta", "delta": …}`` — a chunk of answer
|
|
11
|
+
text.
|
|
12
|
+
- ``{"type": "response.reasoning_text.delta" | "response.reasoning.delta",
|
|
13
|
+
"delta": …}`` — a chunk of reasoning text.
|
|
14
|
+
- ``{"type": "response.output_item.done", "item": …}`` /
|
|
15
|
+
``{"type": "item.completed", "item": …}`` — a completed item; a
|
|
16
|
+
``function_call`` / ``tool_call`` item becomes a fully-formed tool call
|
|
17
|
+
(its ``arguments`` string is parsed into an object).
|
|
18
|
+
- ``{"type": "turn.completed" | "response.completed"}`` — the turn settled;
|
|
19
|
+
finish (tool-bearing turns settle as ``toolUse``, else ``stop``).
|
|
20
|
+
- ``{"type": "error" | "turn.failed", "error" | "message": …}`` — a runtime
|
|
21
|
+
fault.
|
|
22
|
+
|
|
23
|
+
Item-level ``*.delta`` text is preferred; if a CLI variant only emits whole
|
|
24
|
+
items, the completed-item handler still surfaces the text. The injected
|
|
25
|
+
``ChildTransport`` carries the events — no ``codex`` binary is spawned here.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
from collections.abc import Mapping
|
|
32
|
+
from typing import Any, Final
|
|
33
|
+
|
|
34
|
+
from induscode.runtime_bridge.contract import (
|
|
35
|
+
AssistantMessageEventStream,
|
|
36
|
+
BridgeEventSink,
|
|
37
|
+
BridgeFailure,
|
|
38
|
+
ChildMessage,
|
|
39
|
+
ChildRequest,
|
|
40
|
+
ChildTransport,
|
|
41
|
+
Context,
|
|
42
|
+
ExchangeOptions,
|
|
43
|
+
ExternalRuntimeSpec,
|
|
44
|
+
FinishReason,
|
|
45
|
+
Model,
|
|
46
|
+
ResumeEvent,
|
|
47
|
+
RuntimeAdapterId,
|
|
48
|
+
RuntimeBridge,
|
|
49
|
+
ToolCall,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
from ._drive import CONTINUE, DONE, ChildParser, ParseStep, as_record, drive_exchange, str_field
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
"codex_cli_bridge",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
#: The adapter id this bridge answers to.
|
|
59
|
+
_ADAPTER: Final = "codex-cli"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _parse_arguments(value: Any) -> Mapping[str, Any]:
|
|
63
|
+
"""Parse a ``--json`` item's ``arguments`` (a JSON string or an object)
|
|
64
|
+
into a record."""
|
|
65
|
+
if isinstance(value, str):
|
|
66
|
+
if len(value) == 0:
|
|
67
|
+
return {}
|
|
68
|
+
try:
|
|
69
|
+
parsed: Any = json.loads(value)
|
|
70
|
+
except (ValueError, TypeError):
|
|
71
|
+
return {}
|
|
72
|
+
return as_record(parsed) or {}
|
|
73
|
+
return as_record(value) or {}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _tool_call_from_item(item: Mapping[str, Any]) -> ToolCall | None:
|
|
77
|
+
"""Build a ``ToolCall`` from a completed ``function_call`` /
|
|
78
|
+
``tool_call`` item."""
|
|
79
|
+
name = str_field(item, "name")
|
|
80
|
+
if name is None:
|
|
81
|
+
return None
|
|
82
|
+
id_ = str_field(item, "call_id") or str_field(item, "id") or name
|
|
83
|
+
return ToolCall(id=id_, name=name, arguments=_parse_arguments(item.get("arguments")))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _is_tool_item(item: Mapping[str, Any]) -> bool:
|
|
87
|
+
"""True when a completed item is a tool invocation."""
|
|
88
|
+
item_type = str_field(item, "type")
|
|
89
|
+
return item_type == "function_call" or item_type == "tool_call"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _make_parser() -> ChildParser:
|
|
93
|
+
"""Parse one ``--json`` event into sink emissions. Tracks whether any
|
|
94
|
+
tool call was surfaced so the terminal ``turn.completed`` can settle as
|
|
95
|
+
``toolUse``."""
|
|
96
|
+
saw_tool_call = False
|
|
97
|
+
|
|
98
|
+
def handle_completed_item(item: Mapping[str, Any], sink: BridgeEventSink) -> None:
|
|
99
|
+
nonlocal saw_tool_call
|
|
100
|
+
if _is_tool_item(item):
|
|
101
|
+
call = _tool_call_from_item(item)
|
|
102
|
+
if call is not None:
|
|
103
|
+
sink.tool_call(call)
|
|
104
|
+
saw_tool_call = True
|
|
105
|
+
return
|
|
106
|
+
# A non-tool completed item may carry the full text for CLI variants
|
|
107
|
+
# that do not stream `output_text.delta`; surface it if present and
|
|
108
|
+
# non-empty.
|
|
109
|
+
text = str_field(item, "text")
|
|
110
|
+
if text is not None and len(text) > 0:
|
|
111
|
+
sink.text(text)
|
|
112
|
+
|
|
113
|
+
def parse(message: ChildMessage, sink: BridgeEventSink) -> ParseStep:
|
|
114
|
+
payload = as_record(message.payload)
|
|
115
|
+
if payload is None:
|
|
116
|
+
return CONTINUE
|
|
117
|
+
type_ = str_field(payload, "type")
|
|
118
|
+
|
|
119
|
+
match type_:
|
|
120
|
+
case "thread.started":
|
|
121
|
+
thread_id = str_field(payload, "thread_id")
|
|
122
|
+
if thread_id is not None:
|
|
123
|
+
sink.emit(ResumeEvent(resumeToken=thread_id))
|
|
124
|
+
return CONTINUE
|
|
125
|
+
|
|
126
|
+
case "response.output_text.delta":
|
|
127
|
+
delta = str_field(payload, "delta")
|
|
128
|
+
if delta is not None:
|
|
129
|
+
sink.text(delta)
|
|
130
|
+
return CONTINUE
|
|
131
|
+
|
|
132
|
+
case "response.reasoning_text.delta" | "response.reasoning.delta":
|
|
133
|
+
delta = str_field(payload, "delta")
|
|
134
|
+
if delta is not None:
|
|
135
|
+
sink.thinking(delta)
|
|
136
|
+
return CONTINUE
|
|
137
|
+
|
|
138
|
+
case "response.output_item.done" | "item.completed":
|
|
139
|
+
item = as_record(payload.get("item"))
|
|
140
|
+
if item is not None:
|
|
141
|
+
handle_completed_item(item, sink)
|
|
142
|
+
return CONTINUE
|
|
143
|
+
|
|
144
|
+
case "error" | "turn.failed":
|
|
145
|
+
error = as_record(payload.get("error"))
|
|
146
|
+
detail = (
|
|
147
|
+
(str_field(error, "message") if error is not None else None)
|
|
148
|
+
or str_field(payload, "message")
|
|
149
|
+
or "codex-cli error"
|
|
150
|
+
)
|
|
151
|
+
cause = payload.get("error")
|
|
152
|
+
sink.finish_error(
|
|
153
|
+
BridgeFailure(message=detail, cause=cause if cause is not None else payload)
|
|
154
|
+
)
|
|
155
|
+
return DONE
|
|
156
|
+
|
|
157
|
+
case "turn.completed" | "response.completed":
|
|
158
|
+
reason: FinishReason = "toolUse" if saw_tool_call else "stop"
|
|
159
|
+
sink.finish_success(reason)
|
|
160
|
+
return DONE
|
|
161
|
+
|
|
162
|
+
case _:
|
|
163
|
+
return CONTINUE
|
|
164
|
+
|
|
165
|
+
return parse
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _opening_request(context: Context, opts: ExchangeOptions) -> ChildRequest:
|
|
169
|
+
"""Build the opening request: the conversation context as the CLI turn
|
|
170
|
+
input."""
|
|
171
|
+
return ChildRequest(
|
|
172
|
+
body={
|
|
173
|
+
"type": "turn",
|
|
174
|
+
"context": context,
|
|
175
|
+
"thread": opts.resume,
|
|
176
|
+
"cwd": opts.cwd,
|
|
177
|
+
"sessionId": opts.sessionId,
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class _CodexCliBridge:
|
|
183
|
+
"""The ``codex-cli`` :class:`RuntimeBridge`."""
|
|
184
|
+
|
|
185
|
+
adapter: RuntimeAdapterId = _ADAPTER
|
|
186
|
+
|
|
187
|
+
def run_exchange(
|
|
188
|
+
self,
|
|
189
|
+
model: Model,
|
|
190
|
+
context: Context,
|
|
191
|
+
opts: ExchangeOptions,
|
|
192
|
+
transport: ChildTransport,
|
|
193
|
+
) -> AssistantMessageEventStream:
|
|
194
|
+
return drive_exchange(
|
|
195
|
+
model, opts, transport, _opening_request(context, opts), _make_parser()
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def requires_credential(self, spec: ExternalRuntimeSpec) -> bool:
|
|
199
|
+
return spec.authMode == "api-key"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
#: The shipped ``codex-cli`` bridge singleton.
|
|
203
|
+
codex_cli_bridge: RuntimeBridge = _CodexCliBridge()
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""``indusagi-cli`` bridge — drives a peer agent over JSON-RPC (port of TS
|
|
2
|
+
``src/runtime-bridge/bridges/indusagi-cli.ts``).
|
|
3
|
+
|
|
4
|
+
Unlike the CLI bridges, the peer already speaks (a transport-framed form of)
|
|
5
|
+
the framework event vocabulary, so its messages map onto
|
|
6
|
+
:data:`~induscode.runtime_bridge.contract.NormalizedEvent` near-directly.
|
|
7
|
+
Each inbound ``ChildMessage.payload`` is a JSON-RPC frame:
|
|
8
|
+
|
|
9
|
+
- a **notification** ``{"method", "params"}`` streams one turn event:
|
|
10
|
+
|
|
11
|
+
- ``stream/text`` params ``{delta}`` → ``text``
|
|
12
|
+
- ``stream/thinking`` params ``{delta}`` → ``thinking``
|
|
13
|
+
- ``stream/toolCall`` params ``{id, name, arguments}`` → ``tool_call``
|
|
14
|
+
- ``session/resume`` params ``{resumeToken}`` → ``resume``
|
|
15
|
+
- ``stream/done`` params ``{reason}`` → ``finish``
|
|
16
|
+
- ``stream/error`` params ``{message, aborted}`` → ``failed``
|
|
17
|
+
|
|
18
|
+
- a **response** ``{"id", "result" | "error"}`` to the opening
|
|
19
|
+
``runExchange`` request: an ``error`` member fails the exchange; a
|
|
20
|
+
``result`` is treated as a terminal acknowledgement (``finish``) if the
|
|
21
|
+
stream has not already settled.
|
|
22
|
+
|
|
23
|
+
The opening request is a JSON-RPC ``runExchange`` call carrying the context,
|
|
24
|
+
delegate provider, and resume token. The peer is reached only through the
|
|
25
|
+
injected ``ChildTransport``; no peer process is spawned here.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from collections.abc import Mapping
|
|
31
|
+
from typing import Any, Final
|
|
32
|
+
|
|
33
|
+
from induscode.runtime_bridge.contract import (
|
|
34
|
+
AssistantMessageEventStream,
|
|
35
|
+
BridgeEventSink,
|
|
36
|
+
BridgeFailure,
|
|
37
|
+
ChildMessage,
|
|
38
|
+
ChildRequest,
|
|
39
|
+
ChildTransport,
|
|
40
|
+
Context,
|
|
41
|
+
ExchangeOptions,
|
|
42
|
+
ExternalRuntimeSpec,
|
|
43
|
+
FinishReason,
|
|
44
|
+
Model,
|
|
45
|
+
ResumeEvent,
|
|
46
|
+
RuntimeAdapterId,
|
|
47
|
+
RuntimeBridge,
|
|
48
|
+
ToolCall,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
from ._drive import CONTINUE, DONE, ChildParser, ParseStep, as_record, drive_exchange, str_field
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
"indusagi_cli_bridge",
|
|
55
|
+
"make_indusagi_cli_bridge",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
#: The adapter id this bridge answers to.
|
|
59
|
+
_ADAPTER: Final = "indusagi-cli"
|
|
60
|
+
|
|
61
|
+
#: The JSON-RPC method invoked to start a peer exchange.
|
|
62
|
+
_RPC_METHOD: Final = "runExchange"
|
|
63
|
+
|
|
64
|
+
#: The peer's finish reasons are the framework's own; narrow defensively.
|
|
65
|
+
_ALLOWED_REASONS: Final[tuple[FinishReason, ...]] = ("stop", "length", "toolUse")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _as_finish_reason(raw: str | None) -> FinishReason:
|
|
69
|
+
return raw if raw in _ALLOWED_REASONS else "stop" # type: ignore[return-value]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _tool_call_from_params(params: Mapping[str, Any]) -> ToolCall | None:
|
|
73
|
+
"""Build a ``ToolCall`` from a ``stream/toolCall`` notification's params."""
|
|
74
|
+
id_ = str_field(params, "id")
|
|
75
|
+
name = str_field(params, "name")
|
|
76
|
+
if id_ is None or name is None:
|
|
77
|
+
return None
|
|
78
|
+
return ToolCall(id=id_, name=name, arguments=as_record(params.get("arguments")) or {})
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _handle_notification(
|
|
82
|
+
method: str,
|
|
83
|
+
params: Mapping[str, Any] | None,
|
|
84
|
+
sink: BridgeEventSink,
|
|
85
|
+
) -> ParseStep:
|
|
86
|
+
"""Dispatch one JSON-RPC notification by method."""
|
|
87
|
+
match method:
|
|
88
|
+
case "stream/text":
|
|
89
|
+
delta = str_field(params, "delta") if params is not None else None
|
|
90
|
+
if delta is not None:
|
|
91
|
+
sink.text(delta)
|
|
92
|
+
return CONTINUE
|
|
93
|
+
case "stream/thinking":
|
|
94
|
+
delta = str_field(params, "delta") if params is not None else None
|
|
95
|
+
if delta is not None:
|
|
96
|
+
sink.thinking(delta)
|
|
97
|
+
return CONTINUE
|
|
98
|
+
case "stream/toolCall":
|
|
99
|
+
call = _tool_call_from_params(params) if params is not None else None
|
|
100
|
+
if call is not None:
|
|
101
|
+
sink.tool_call(call)
|
|
102
|
+
return CONTINUE
|
|
103
|
+
case "session/resume":
|
|
104
|
+
token = str_field(params, "resumeToken") if params is not None else None
|
|
105
|
+
if token is not None:
|
|
106
|
+
sink.emit(ResumeEvent(resumeToken=token))
|
|
107
|
+
return CONTINUE
|
|
108
|
+
case "stream/done":
|
|
109
|
+
sink.finish_success(
|
|
110
|
+
_as_finish_reason(str_field(params, "reason") if params is not None else None)
|
|
111
|
+
)
|
|
112
|
+
return DONE
|
|
113
|
+
case "stream/error":
|
|
114
|
+
message = (
|
|
115
|
+
str_field(params, "message") if params is not None else None
|
|
116
|
+
) or "indusagi-cli error"
|
|
117
|
+
aborted = params is not None and params.get("aborted") is True
|
|
118
|
+
sink.finish_error(BridgeFailure(message=message, aborted=aborted, cause=params))
|
|
119
|
+
return DONE
|
|
120
|
+
case _:
|
|
121
|
+
return CONTINUE
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _parse_frame(message: ChildMessage, sink: BridgeEventSink) -> ParseStep:
|
|
125
|
+
"""Parse one JSON-RPC frame (notification or response) into sink
|
|
126
|
+
emissions."""
|
|
127
|
+
frame = as_record(message.payload)
|
|
128
|
+
if frame is None:
|
|
129
|
+
return CONTINUE
|
|
130
|
+
|
|
131
|
+
# A notification carries a `method`; a response carries `result` / `error`.
|
|
132
|
+
method = str_field(frame, "method")
|
|
133
|
+
if method is not None:
|
|
134
|
+
return _handle_notification(method, as_record(frame.get("params")), sink)
|
|
135
|
+
|
|
136
|
+
# Response to the opening call.
|
|
137
|
+
if "error" in frame:
|
|
138
|
+
error = as_record(frame.get("error"))
|
|
139
|
+
detail = (
|
|
140
|
+
str_field(error, "message") if error is not None else None
|
|
141
|
+
) or "indusagi-cli rpc error"
|
|
142
|
+
sink.finish_error(BridgeFailure(message=detail, cause=frame.get("error")))
|
|
143
|
+
return DONE
|
|
144
|
+
if "result" in frame:
|
|
145
|
+
# A bare result acknowledgement settles the exchange if streaming
|
|
146
|
+
# notifications did not already emit a terminal `stream/done`.
|
|
147
|
+
return DONE
|
|
148
|
+
return CONTINUE
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
#: The reusable parser (no per-exchange state; the frame carries everything).
|
|
152
|
+
_parser: ChildParser = _parse_frame
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _opening_request(
|
|
156
|
+
context: Context, opts: ExchangeOptions, spec: ExternalRuntimeSpec
|
|
157
|
+
) -> ChildRequest:
|
|
158
|
+
"""Build the opening JSON-RPC ``runExchange`` request."""
|
|
159
|
+
return ChildRequest(
|
|
160
|
+
body={
|
|
161
|
+
"jsonrpc": "2.0",
|
|
162
|
+
"id": opts.sessionId if opts.sessionId is not None else _RPC_METHOD,
|
|
163
|
+
"method": _RPC_METHOD,
|
|
164
|
+
"params": {
|
|
165
|
+
"context": context,
|
|
166
|
+
"delegate": spec.delegate,
|
|
167
|
+
"resume": opts.resume,
|
|
168
|
+
"cwd": opts.cwd,
|
|
169
|
+
"sessionId": opts.sessionId,
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class _IndusagiCliBridge:
|
|
176
|
+
"""An ``indusagi-cli`` :class:`RuntimeBridge` bound to one spec (so the
|
|
177
|
+
opening request can forward its ``delegate`` to the peer)."""
|
|
178
|
+
|
|
179
|
+
adapter: RuntimeAdapterId = _ADAPTER
|
|
180
|
+
|
|
181
|
+
__slots__ = ("_spec",)
|
|
182
|
+
|
|
183
|
+
def __init__(self, spec: ExternalRuntimeSpec) -> None:
|
|
184
|
+
self._spec = spec
|
|
185
|
+
|
|
186
|
+
def run_exchange(
|
|
187
|
+
self,
|
|
188
|
+
model: Model,
|
|
189
|
+
context: Context,
|
|
190
|
+
opts: ExchangeOptions,
|
|
191
|
+
transport: ChildTransport,
|
|
192
|
+
) -> AssistantMessageEventStream:
|
|
193
|
+
return drive_exchange(
|
|
194
|
+
model, opts, transport, _opening_request(context, opts, self._spec), _parser
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def requires_credential(self, spec: ExternalRuntimeSpec) -> bool:
|
|
198
|
+
return spec.authMode == "api-key"
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def make_indusagi_cli_bridge(spec: ExternalRuntimeSpec) -> RuntimeBridge:
|
|
202
|
+
"""Build the ``indusagi-cli`` :class:`RuntimeBridge` (TS
|
|
203
|
+
``makeIndusagiCliBridge``). The bound :class:`ExternalRuntimeSpec` is
|
|
204
|
+
captured so the opening request can forward its ``delegate`` to the peer;
|
|
205
|
+
the broker constructs one bridge per spec it routes to.
|
|
206
|
+
|
|
207
|
+
:param spec: the runtime annotation whose ``delegate`` the peer should target
|
|
208
|
+
"""
|
|
209
|
+
return _IndusagiCliBridge(spec)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
#: A default ``indusagi-cli`` bridge bound to a spec with no delegate. Use
|
|
213
|
+
#: :func:`make_indusagi_cli_bridge` when a delegate provider must be
|
|
214
|
+
#: forwarded.
|
|
215
|
+
indusagi_cli_bridge: RuntimeBridge = make_indusagi_cli_bridge(
|
|
216
|
+
ExternalRuntimeSpec(adapter=_ADAPTER, authMode="external-cli")
|
|
217
|
+
)
|