keepai 0.6.1__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.
keep/__init__.py ADDED
File without changes
keep/agent.py ADDED
@@ -0,0 +1,72 @@
1
+ """Agent abstraction — entry point for multi-agent support (Plan #14 walking skeleton).
2
+
3
+ This module defines the `Agent` Protocol that keep's daemon and CLI will
4
+ talk to once multi-agent support is fully wired up. Plan #11 designed the
5
+ full 5-method interface; Plan #14 is a spike that implements **only** the
6
+ `last_message()` method to validate the abstraction is buildable.
7
+
8
+ Concrete implementations live under `keep.agents.*`:
9
+ - `keep.agents.*` — Claude Code implementation (this plan)
10
+ - (future) `keep.agents.*` — OpenAI Codex CLI
11
+
12
+ Design discrepancy from Plan #11 §3.1 captured in the walking skeleton
13
+ report: `AgentMessage.ts` is typed `str | None` here, not `datetime | None`
14
+ as originally specced. Reason: the current CC JSONL timestamp format
15
+ (`2026-04-09T01:55:03.323Z`) does not round-trip byte-identically through
16
+ Python's `datetime.isoformat()` (pads microseconds to 6 digits, rewrites
17
+ `Z` to `+00:00`). The `keep read` CLI must stay byte-identical per Plan
18
+ #14's acceptance criterion, and preserving the raw source string is the
19
+ least invasive way to satisfy both Plan #14 and Plan #11's spirit.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from typing import Protocol
25
+
26
+ from pydantic import BaseModel, ConfigDict
27
+
28
+
29
+ class AgentMessage(BaseModel):
30
+ """Single assistant reply from an agent session."""
31
+
32
+ model_config = ConfigDict(extra="forbid")
33
+
34
+ text: str
35
+ ts: str | None = None
36
+ """Raw ISO8601 timestamp string from the source, preserved verbatim.
37
+
38
+ Deliberately `str`, not `datetime`: CC JSONL writes `2026-04-09T01:55:03.323Z`
39
+ but `datetime.fromisoformat(...).isoformat()` round-trips to
40
+ `2026-04-09T01:55:03.323000+00:00` — not byte-identical. Plan #14 accepts
41
+ this field-level deviation from Plan #11 §3.1 so the `keep read` CLI can
42
+ re-emit the timestamp unchanged. Plan #15+ may revisit.
43
+ """
44
+
45
+ turn_id: str | None = None
46
+ """Opaque, agent-specific identifier for cross-referencing turns.
47
+ Unused in Plan #14; reserved for future use."""
48
+
49
+
50
+ class Agent(Protocol):
51
+ """Protocol for an AI coding agent that keep orchestrates.
52
+
53
+ Plan #14 walking skeleton only implements `last_message()`. The other
54
+ four methods from Plan #11 §3.2 (`status`, `poll_events`, `send`,
55
+ `interrupt`) are intentionally **not** declared here yet — adding them
56
+ as stubs would force every implementation to `NotImplementedError`
57
+ them, which defeats the point of Protocol typing. They will be added
58
+ in subsequent plans once their own walking skeletons pass.
59
+ """
60
+
61
+ def last_message(self, session: str) -> AgentMessage | None:
62
+ """Return the most recent assistant reply for `session`, or None.
63
+
64
+ None means no retrievable message: session log missing, malformed,
65
+ empty, or no assistant entries yet. Query method — never raises;
66
+ all errors collapse to None. Idempotent and non-consuming.
67
+
68
+ `session` is an agent-specific identifier (CC session name or
69
+ UUID, Codex thread_id, etc.). The concrete `Agent` implementation
70
+ interprets it.
71
+ """
72
+ ...
@@ -0,0 +1,7 @@
1
+ """Concrete `Agent` implementations.
2
+
3
+ Each submodule hosts one implementation of `keep.agent.Agent`:
4
+
5
+ - `cc` — Claude Code (this plan)
6
+ - (future) `codex` — OpenAI Codex CLI (Plan #17 per Plan #11 §9)
7
+ """
keep/agents/cc.py ADDED
@@ -0,0 +1,75 @@
1
+ """Claude Code implementation of `keep.agent.Agent`.
2
+
3
+ Plan #14 walking skeleton — only `last_message()` is implemented. The impl
4
+ wraps the existing `keep.worker.resolve_worker` + `read_last_message`
5
+ helpers rather than duplicating their logic, so behavior stays consistent
6
+ with the pre-abstraction `keep read` command.
7
+
8
+ Plan #14 Part 3 requires the `last_message` read path to be exercisable on
9
+ my-laptop (Linux Python 3.12.3, no cmux installed). `resolve_worker`
10
+ unconditionally requires cmux env vars (CMUX_WORKSPACE_ID /
11
+ CMUX_SURFACE_ID) and `ps` reachability for the CC process, which is a hard
12
+ block on cmux-less environments. To keep the Protocol signature clean
13
+ while making Part 3 possible, `last_message()` accepts an **optional**
14
+ `jsonl_path` keyword argument that short-circuits `resolve_worker`. The
15
+ Protocol itself only advertises `last_message(session)`; the keyword arg
16
+ is a CC-impl-specific dependency-injection hatch for tests and for
17
+ cmux-less realistic testing.
18
+
19
+ This DI hatch is **the minimum deviation from Plan #11 §3.2** required to
20
+ make the walking skeleton testable end-to-end without cmux. It is called
21
+ out explicitly in the Plan #14 report.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from pathlib import Path
27
+
28
+ from keep.agent import AgentMessage
29
+ from keep.worker import read_last_message, resolve_worker
30
+
31
+
32
+ class CCAgent:
33
+ """Plan #14 walking skeleton CCAgent — implements `Agent.last_message` only."""
34
+
35
+ name = "claude-code"
36
+
37
+ def last_message(
38
+ self,
39
+ session: str,
40
+ *,
41
+ jsonl_path: Path | str | None = None,
42
+ ) -> AgentMessage | None:
43
+ """Return the most recent assistant reply for a CC session.
44
+
45
+ Resolution order for the JSONL file:
46
+
47
+ 1. If `jsonl_path` is provided explicitly, use it directly. This is
48
+ the DI hatch for tests and for cmux-less realistic testing (see
49
+ module docstring).
50
+ 2. Otherwise call `resolve_worker(session)` to discover the JSONL
51
+ via the `ps` + cmux env var path used by the rest of keep.
52
+
53
+ Returns None for any failure mode — missing file, malformed file,
54
+ no assistant entries yet, or unreachable session. Query method,
55
+ never raises. All three error branches of the underlying
56
+ `worker.read_last_message` collapse into None (see Plan #14 report
57
+ for the specific CLI error-message specificity loss this causes
58
+ in `keep read`).
59
+ """
60
+ if jsonl_path is not None:
61
+ path = Path(jsonl_path)
62
+ else:
63
+ info = resolve_worker(session)
64
+ if info.jsonl_path is None:
65
+ return None
66
+ path = info.jsonl_path
67
+
68
+ result = read_last_message(path)
69
+ if not result.get("ok"):
70
+ return None
71
+
72
+ return AgentMessage(
73
+ text=result["message"],
74
+ ts=result.get("timestamp") or None,
75
+ )