Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/canary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ jobs:

- name: Install bridge + latest SDK
run: |
pip install -e . pytest
pip install \
"inkbox @ git+https://github.com/inkbox-ai/inkbox.git@fdea6e55ac117f246624852aedeef0feabe08b9a#subdirectory=sdk/python" \
-e . pytest
pip install -U claude-agent-sdk

- name: Install latest Claude Code CLI
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/live-channels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ jobs:
node-version: 22

- name: Install bridge
run: pip install -e . pytest
run: |
pip install \
"inkbox @ git+https://github.com/inkbox-ai/inkbox.git@fdea6e55ac117f246624852aedeef0feabe08b9a#subdirectory=sdk/python" \
-e . pytest

- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/live-external-events.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ jobs:
node-version: 22

- name: Install bridge
run: pip install -e . pytest
run: |
pip install \
"inkbox @ git+https://github.com/inkbox-ai/inkbox.git@fdea6e55ac117f246624852aedeef0feabe08b9a#subdirectory=sdk/python" \
-e . pytest

- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/live-voice.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ jobs:
# uvicorn[standard] matters: the bare install can't accept WebSocket
# upgrades, and the driver's call-media endpoint is a WebSocket.
- name: Install bridge + driver deps
run: pip install -e . pytest fastapi 'uvicorn[standard]'
run: |
pip install \
"inkbox @ git+https://github.com/inkbox-ai/inkbox.git@fdea6e55ac117f246624852aedeef0feabe08b9a#subdirectory=sdk/python" \
-e . pytest fastapi 'uvicorn[standard]'

- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:
- name: Install bridge + latest SDK
run: |
uv pip install --system \
"inkbox @ git+https://github.com/inkbox-ai/inkbox.git@199bbd27c8dab2f70e379de52ca9cc910b0e141d#subdirectory=sdk/python" \
"inkbox @ git+https://github.com/inkbox-ai/inkbox.git@fdea6e55ac117f246624852aedeef0feabe08b9a#subdirectory=sdk/python" \
-e . pytest
uv pip install --system -U claude-agent-sdk

Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,11 @@ The agent reaches you (or third parties) through an in-process MCP server:
- `inkbox_list_imessage_conversations` · `inkbox_get_imessage_conversation` — browse iMessage threads and history (find the `conversation_id` to send into).
- `inkbox_lookup_contact` · `inkbox_list_contacts` · `inkbox_get_contact` — resolve and read address-book contacts (reverse-lookup by email/phone, free-text search, or full record by id).
- `inkbox_create_contact` · `inkbox_update_contact` · `inkbox_delete_contact` — save, edit, and remove organization-wide contacts. Changes affect the shared address book. vCard export/import is not exposed.
- `inkbox_a2a_call` · `inkbox_a2a_check` · `inkbox_a2a_reply` — delegate work to another agent and follow its task.
- `inkbox_list_a2a_tasks` · `inkbox_list_a2a_messages` — page and search this identity's inbound and outbound A2A history, with participant, task, context, role, state, and timestamp filters.
- `inkbox_a2a_complete` · `inkbox_a2a_ask_caller` · `inkbox_a2a_fail` — commit the outcome of a verified inbound A2A task. These tools are rejected outside that task's isolated session.

The bridge requires Inkbox SDK 0.5.6 or newer.

On a live call, the OpenAI Realtime voice agent additionally gets `consult_agent`, `register_post_call_action` / `edit_post_call_action` / `delete_post_call_action`, and `hang_up_call` — see [Voice](#voice).

Expand Down
106 changes: 106 additions & 0 deletions inkbox_claude/a2a_delegations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Durable caller-side A2A delegation routing records."""

from __future__ import annotations

import json
import os
import threading
import time
from pathlib import Path
from typing import Any, Dict, Optional
from urllib.parse import urlsplit

_LOCK = threading.Lock()


def _path() -> Path:
root = Path(
os.getenv("INKBOX_CLAUDE_HOME")
or (Path.home() / ".inkbox-claude")
)
return root / "a2a_delegations.json"


def _origin(url: str) -> str:
parsed = urlsplit(url)
return f"{parsed.scheme.lower()}://{parsed.netloc.lower()}"


def _read() -> Dict[str, Dict[str, Any]]:
try:
loaded = json.loads(_path().read_text())
return loaded if isinstance(loaded, dict) else {}
except FileNotFoundError:
return {}


def _write(records: Dict[str, Dict[str, Any]]) -> None:
path = _path()
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(".tmp")
tmp.write_text(json.dumps(records, indent=2, sort_keys=True) + "\n")
tmp.chmod(0o600)
os.replace(tmp, path)
path.chmod(0o600)


def record_before_send(
*,
identity_id: str,
rpc_url: str,
card_url: str,
message_id: str,
session_key: Optional[str],
context_id: Optional[str] = None,
task_id: Optional[str] = None,
) -> str:
"""Persist retry and routing data before the SendMessage request."""
origin = _origin(rpc_url)
context_key = context_id or f"pending:{message_id}"
key = f"{identity_id}|{origin}|{context_key}"
with _LOCK:
records = _read()
records[key] = {
"identity_id": identity_id,
"origin": origin,
"card_url": card_url,
"context_id": context_id,
"task_id": task_id,
"message_id": message_id,
"session_key": session_key,
"updated_at": time.time(),
}
_write(records)
return key


def promote_after_send(
pending_key: str,
*,
context_id: str,
task_id: str,
) -> None:
"""Promote a provisional record to its canonical context key."""
with _LOCK:
records = _read()
record = records.pop(pending_key, None)
if record is None:
return
record["context_id"] = context_id
record["task_id"] = task_id
record["updated_at"] = time.time()
key = (
f"{record['identity_id']}|{record['origin']}|{context_id}"
)
records[key] = record
_write(records)


def find_by_task(task_id: str) -> Optional[Dict[str, Any]]:
"""Return the newest local delegation record for a remote task."""
matches = [
record
for record in _read().values()
if str(record.get("task_id") or "") == task_id
]
return max(matches, key=lambda item: item.get("updated_at", 0), default=None)
2 changes: 1 addition & 1 deletion inkbox_claude/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def run_doctor() -> List[Tuple[str, bool, str]]:
import inkbox # noqa: F401
checks.append(("inkbox SDK", True, "installed"))
except ImportError:
checks.append(("inkbox SDK", False, "pip install 'inkbox>=0.5.1,<1.0.0'"))
checks.append(("inkbox SDK", False, "pip install 'inkbox>=0.5.6,<1.0.0'"))

try:
import claude_agent_sdk # noqa: F401
Expand Down
Loading