docs: spec v3 — drop Tree-Nav from iOS, audit closed
- Audit confirmed S-07, S-08 work out-of-the-box → upgraded from PENDING to firm SHOULD - Tree-Write blocked by ExtensionAPI; Gruppe T removed entirely - Tree-Navigation explicitly out of scope (native pi only) - Spike-0a closed, EXTENSION-API-AUDIT.md is referenced artifact
This commit is contained in:
parent
36938a66c4
commit
cf61b2ba1b
|
|
@ -0,0 +1,936 @@
|
||||||
|
# pi ExtensionAPI Audit (für iOS-App-Spec v2)
|
||||||
|
|
||||||
|
**Datum:** 2026-05-15
|
||||||
|
**pi Version:** 0.74.0
|
||||||
|
**Auditor:** Subagent (Claude Sonnet 4.6)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
- ✅ **S-07 State Side-Channel** — machbar mit Einschränkungen (kein explizites `awaiting-input` Event, muss abgeleitet werden)
|
||||||
|
- ✅ **S-08 Slash-Command-Registry** — direkt verfügbar via `pi.getCommands()`
|
||||||
|
- ⚠️ **S-13 Tree-State Side-Channel** — Tree-Read funktioniert, aber keine Subscribe-Mechanik für Live-Updates (Polling oder Event-basiert möglich)
|
||||||
|
- ❌ **Gruppe T Tree-Navigation** — Slash-Command-Injection funktioniert NICHT (Commands werden als Text an LLM gesendet, nicht dispatched). Hack via `(pi as any)` nötig oder Upstream-Change.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Methodik
|
||||||
|
|
||||||
|
**Untersuchte Quellen:**
|
||||||
|
- pi-Installation: `/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/` (v0.74.0)
|
||||||
|
- TypeScript-Definitionen: `dist/core/extensions/types.d.ts` (1173 Zeilen)
|
||||||
|
- Session-Manager: `dist/core/session-manager.d.ts` (308 Zeilen)
|
||||||
|
- Slash-Commands: `dist/core/slash-commands.d.ts` (15 Zeilen)
|
||||||
|
- Offizielle Doku: `docs/extensions.md` (97KB)
|
||||||
|
- Bestehende Extension: `/Users/jay/.pi/agent/git/git.vpsj.de/jay/pi-remote-control/extensions/remote-control/`
|
||||||
|
|
||||||
|
**Verifikationsmethode:** Statische Analyse der TypeScript-Definitionen + Cross-Check gegen bestehenden Code der `pi-remote-control` Extension.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Capability-Matrix
|
||||||
|
|
||||||
|
| Capability | Status | Spec-Feature | Workaround | Aufwand |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **Lifecycle Events** | ✅ Vorhanden | S-07 | `agent_end` statt `awaiting-input` | Trivial |
|
||||||
|
| **Tool Execution Events** | ✅ Vorhanden | S-07 | — | Trivial |
|
||||||
|
| **Slash-Command-Registry** | ✅ Vorhanden | S-08 | — | Trivial |
|
||||||
|
| **Tree-Read** | ✅ Vorhanden | S-13 | — | Trivial |
|
||||||
|
| **Tree-Subscribe (Live-Updates)** | ⚠️ Event-basiert | S-13 | Events tracken statt Subscribe | Einfach |
|
||||||
|
| **Slash-Command-Dispatch** | ❌ Nicht exposed | Gruppe T | `(pi as any)` Hack | Fragil |
|
||||||
|
| **Tool-Call-Details** | ✅ Vorhanden | S-07 | — | Trivial |
|
||||||
|
|
||||||
|
**Legende:**
|
||||||
|
- ✅ = Out-of-the-box verfügbar
|
||||||
|
- ⚠️ = Machbar mit Einschränkungen
|
||||||
|
- ❌ = Nicht ohne Hack oder Upstream-Change
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Capabilities im Detail
|
||||||
|
|
||||||
|
### 3.1 Lifecycle Events
|
||||||
|
|
||||||
|
**Was existiert:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Quelle: dist/core/extensions/types.d.ts, Zeile 458-477
|
||||||
|
pi.on("agent_start", async (event, ctx) => { ... });
|
||||||
|
pi.on("agent_end", async (event, ctx) => { ... });
|
||||||
|
pi.on("tool_execution_start", async (event, ctx) => { ... });
|
||||||
|
pi.on("tool_execution_end", async (event, ctx) => { ... });
|
||||||
|
pi.on("message_start", async (event, ctx) => { ... });
|
||||||
|
pi.on("message_update", async (event, ctx) => { ... });
|
||||||
|
pi.on("message_end", async (event, ctx) => { ... });
|
||||||
|
```
|
||||||
|
|
||||||
|
**Event-Payloads (relevante Felder):**
|
||||||
|
|
||||||
|
| Event | Payload | Code-Beleg |
|
||||||
|
|---|---|---|
|
||||||
|
| `agent_start` | `{ type: "agent_start" }` | types.d.ts:223 |
|
||||||
|
| `agent_end` | `{ type: "agent_end", messages: AgentMessage[] }` | types.d.ts:228 |
|
||||||
|
| `tool_execution_start` | `{ toolCallId, toolName, args }` | types.d.ts:261 |
|
||||||
|
| `tool_execution_end` | `{ toolCallId, toolName, result, isError }` | types.d.ts:275 |
|
||||||
|
| `message_update` | `{ message, assistantMessageEvent }` | types.d.ts:250 |
|
||||||
|
|
||||||
|
**Lücken:**
|
||||||
|
|
||||||
|
- ❌ **Kein explizites `awaiting-input` Event.** Das nächstbeste ist `agent_end`, welches signalisiert dass der Agent-Loop beendet ist. Kombiniert mit `ctx.isIdle()` kann man ableiten, ob Pi auf User-Input wartet.
|
||||||
|
- ❌ **Kein `thinking` vs. `idle` State-Enum.** Muss selbst aus Events abgeleitet werden.
|
||||||
|
|
||||||
|
**Ableitung des Pi-States für S-07:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Pseudo-Code für die Sidecar-Logik
|
||||||
|
let state: "thinking" | "tool" | "idle" | "awaiting-input" = "idle";
|
||||||
|
|
||||||
|
pi.on("agent_start", () => {
|
||||||
|
state = "thinking";
|
||||||
|
broadcast({ type: "state", value: "thinking" });
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.on("tool_execution_start", (event) => {
|
||||||
|
state = "tool";
|
||||||
|
broadcast({ type: "state", value: "tool", tool: event.toolName });
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.on("tool_execution_end", () => {
|
||||||
|
state = "thinking"; // zurück zu thinking nach Tool
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.on("agent_end", (event, ctx) => {
|
||||||
|
if (ctx.isIdle()) {
|
||||||
|
state = "awaiting-input";
|
||||||
|
broadcast({ type: "state", value: "awaiting-input" });
|
||||||
|
} else {
|
||||||
|
state = "thinking"; // Follow-up-Messages pending
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Code-Beleg (bestehende Extension nutzt diese Events):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// pi-remote-control/extensions/remote-control/index.ts, Zeile 89-109
|
||||||
|
pi.on("agent_start", async (_event, ctx) => {
|
||||||
|
server?.broadcast({ type: "agent_start" });
|
||||||
|
updateStatus(ctx);
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.on("agent_end", async (_event, ctx) => {
|
||||||
|
server?.broadcast({ type: "agent_end" });
|
||||||
|
updateStatus(ctx);
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.on("tool_execution_start", async (event) => {
|
||||||
|
server?.broadcast({
|
||||||
|
type: "tool_start",
|
||||||
|
toolCallId: event.toolCallId,
|
||||||
|
toolName: event.toolName,
|
||||||
|
args: event.args,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fazit:** S-07 State-Side-Channel ist **machbar**. Die Granularität ist ausreichend für die Spec-Anforderungen (thinking, tool, idle, awaiting-input). Kein Upstream-Change nötig.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 Slash-Command-Registry
|
||||||
|
|
||||||
|
**Was existiert:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Quelle: dist/core/extensions/types.d.ts, Zeile 772
|
||||||
|
pi.getCommands(): SlashCommandInfo[];
|
||||||
|
```
|
||||||
|
|
||||||
|
**Return-Type:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Quelle: dist/core/slash-commands.d.ts, Zeile 1-9
|
||||||
|
export interface SlashCommandInfo {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
source: "extension" | "prompt" | "skill";
|
||||||
|
sourceInfo: SourceInfo;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Beispiel:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
pi.on("session_start", async (event, ctx) => {
|
||||||
|
const commands = pi.getCommands();
|
||||||
|
// [
|
||||||
|
// { name: "new", description: "Start a new session", source: "extension", ... },
|
||||||
|
// { name: "fork", description: "Fork from an entry", source: "extension", ... },
|
||||||
|
// { name: "tree", description: "Show conversation tree", source: "extension", ... },
|
||||||
|
// { name: "my-custom", description: "...", source: "skill", ... }
|
||||||
|
// ]
|
||||||
|
|
||||||
|
// Für S-08 Endpoint /sessions/<id>/commands:
|
||||||
|
const payload = commands.map(c => ({
|
||||||
|
name: c.name,
|
||||||
|
description: c.description,
|
||||||
|
source: c.source
|
||||||
|
}));
|
||||||
|
// Send via WebSocket or REST
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Code-Beleg:** Die bestehende Extension nutzt `pi.registerCommand()` aber `pi.getCommands()` ist bisher nicht verwendet. Die Funktion ist aber in `types.d.ts:772` klar definiert.
|
||||||
|
|
||||||
|
**Fazit:** S-08 Slash-Command-Registry ist **direkt machbar**, out-of-the-box. Kein Hack, kein Upstream-Change nötig.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 Conversation Tree
|
||||||
|
|
||||||
|
**Was existiert:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Quelle: dist/core/extensions/types.d.ts, Zeile 87
|
||||||
|
ctx.sessionManager: ReadonlySessionManager;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Relevante Methoden (dist/core/session-manager.d.ts):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ReadonlySessionManager {
|
||||||
|
getBranch(fromId?: string): SessionEntry[];
|
||||||
|
getTree(): SessionTreeNode[];
|
||||||
|
getLeafId(): string | null;
|
||||||
|
getEntry(id: string): SessionEntry | undefined;
|
||||||
|
getChildren(parentId: string): SessionEntry[];
|
||||||
|
getSessionName(): string | undefined;
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tree-Structure:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Quelle: session-manager.d.ts, Zeile 74-81
|
||||||
|
export interface SessionTreeNode {
|
||||||
|
entry: SessionEntry;
|
||||||
|
children: SessionTreeNode[];
|
||||||
|
/** Resolved label for this entry, if any */
|
||||||
|
label?: string;
|
||||||
|
/** Timestamp of the latest label change for this entry, if any */
|
||||||
|
labelTimestamp?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**SessionEntry (relevant für Tree-State):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// session-manager.d.ts, Zeile 16-27
|
||||||
|
export interface SessionEntryBase {
|
||||||
|
type: string;
|
||||||
|
id: string;
|
||||||
|
parentId: string | null;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionMessageEntry extends SessionEntryBase {
|
||||||
|
type: "message";
|
||||||
|
message: AgentMessage;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Beispiel für S-13 Tree-State-Endpoint:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
pi.on("session_start", async (event, ctx) => {
|
||||||
|
const tree = ctx.sessionManager.getTree();
|
||||||
|
const currentLeafId = ctx.sessionManager.getLeafId();
|
||||||
|
|
||||||
|
// Tree als JSON für iOS-App serialisieren:
|
||||||
|
function serializeTree(node: SessionTreeNode) {
|
||||||
|
const entry = node.entry;
|
||||||
|
let summary = "";
|
||||||
|
let msgCount = 0;
|
||||||
|
|
||||||
|
if (entry.type === "message") {
|
||||||
|
const msg = (entry as SessionMessageEntry).message;
|
||||||
|
if (msg.role === "user") {
|
||||||
|
const text = typeof msg.content === "string"
|
||||||
|
? msg.content
|
||||||
|
: msg.content.filter(c => c.type === "text").map(c => c.text).join("");
|
||||||
|
summary = text.slice(0, 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count messages in subtree
|
||||||
|
const countMessages = (n: SessionTreeNode): number => {
|
||||||
|
let count = n.entry.type === "message" ? 1 : 0;
|
||||||
|
for (const child of n.children) count += countMessages(child);
|
||||||
|
return count;
|
||||||
|
};
|
||||||
|
msgCount = countMessages(node);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: entry.id,
|
||||||
|
parent: entry.parentId,
|
||||||
|
summary,
|
||||||
|
messageCount: msgCount,
|
||||||
|
createdAt: entry.timestamp,
|
||||||
|
children: node.children.map(serializeTree)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const treeData = {
|
||||||
|
type: "tree",
|
||||||
|
nodes: tree.map(serializeTree),
|
||||||
|
current: currentLeafId
|
||||||
|
};
|
||||||
|
|
||||||
|
// Broadcast oder per REST-Endpoint abrufbar machen
|
||||||
|
broadcast(treeData);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Live-Updates (Subscribe-Problem):**
|
||||||
|
|
||||||
|
Es gibt **kein** `on("tree_update")` Event. Tree-Mutationen passieren nur via:
|
||||||
|
- `/fork` → triggert `session_before_fork` + `session_tree`
|
||||||
|
- `/new` → triggert `session_before_switch`
|
||||||
|
- `/compact` → triggert `session_compact`
|
||||||
|
- Checkout via `/tree` → triggert `session_tree`
|
||||||
|
|
||||||
|
**Events für Tree-Tracking:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Quelle: types.d.ts, Zeile 177-189
|
||||||
|
pi.on("session_tree", async (event, ctx) => {
|
||||||
|
// event = {
|
||||||
|
// newLeafId: string | null,
|
||||||
|
// oldLeafId: string | null,
|
||||||
|
// summaryEntry?: BranchSummaryEntry,
|
||||||
|
// fromExtension?: boolean
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Tree hat sich geändert → re-serialize und broadcasten
|
||||||
|
const updatedTree = serializeTree(ctx.sessionManager.getTree());
|
||||||
|
broadcast({ type: "tree", ...updatedTree });
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.on("session_compact", async (event, ctx) => {
|
||||||
|
// Compaction hat Tree verändert → Update broadcasten
|
||||||
|
const updatedTree = serializeTree(ctx.sessionManager.getTree());
|
||||||
|
broadcast({ type: "tree", ...updatedTree });
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Code-Beleg (bestehende Extension nutzt getBranch):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// pi-remote-control/extensions/remote-control/messages.ts, Zeile 66
|
||||||
|
const branch = ctx.sessionManager.getBranch();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fazit:** S-13 Tree-State ist **machbar mit Event-basiertem Push**. Kein echtes Subscribe-Pattern, aber ausreichend für die Spec. Tree-Read ist trivial, Live-Updates erfordern Event-Handling (`session_tree`, `session_compact`). Kein Upstream-Change nötig.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4 Programmatic Prompt-Dispatch
|
||||||
|
|
||||||
|
**Was existiert:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Quelle: types.d.ts, Zeile 758-763
|
||||||
|
pi.sendUserMessage(
|
||||||
|
content: string | (TextContent | ImageContent)[],
|
||||||
|
options?: { deliverAs?: "steer" | "followUp" }
|
||||||
|
): void;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Was NICHT existiert:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ NICHT in der ExtensionAPI exposed:
|
||||||
|
pi.prompt(text: string): void; // würde Slash-Commands dispatchen
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problem (aus früherer Recherche bestätigt):**
|
||||||
|
|
||||||
|
`pi.sendUserMessage()` ruft intern `session.prompt()` mit `expandPromptTemplates: false` auf, was Slash-Command-Processing überspringt. Slash-Commands wie `/fork`, `/new`, `/compact` werden als **literaler Text an den LLM gesendet**, nicht als Befehle ausgeführt.
|
||||||
|
|
||||||
|
**Code-Beleg (aus vorheriger Session):**
|
||||||
|
|
||||||
|
> Wir hatten in der vorherigen Session festgestellt: `pi.sendUserMessage()` explicitly sets `expandPromptTemplates: false`, bypassing slash command processing. `session.prompt()` (which handles slash commands) is not exposed in `ExtensionAPI`.
|
||||||
|
|
||||||
|
**Workaround-Optionen:**
|
||||||
|
|
||||||
|
#### Option A: Hack via `(pi as any)`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// FRAGIL: greift auf interne APIs zu, kann bei Updates brechen
|
||||||
|
pi.on("some_event", async (event, ctx) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const runtime = (pi as any).runtime;
|
||||||
|
if (runtime && runtime.session) {
|
||||||
|
await runtime.session.prompt("/fork abc123");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Risiken:**
|
||||||
|
- ❌ Keine TypeScript-Typen
|
||||||
|
- ❌ Bricht bei internen Refactorings
|
||||||
|
- ❌ `runtime` könnte `undefined` sein je nach Timing
|
||||||
|
- ❌ Nicht dokumentiert, keine Garantie
|
||||||
|
|
||||||
|
#### Option B: Re-Implement Commands lokal
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// iOS-App sendet Command-Request
|
||||||
|
// Sidecar implementiert /fork, /new, /compact direkt via ExtensionCommandContext
|
||||||
|
pi.registerCommand("ios-fork", {
|
||||||
|
description: "Fork from iOS app",
|
||||||
|
handler: async (args, ctx) => {
|
||||||
|
// args = entryId
|
||||||
|
await ctx.fork(args, { position: "at" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// iOS schickt dann via WebSocket:
|
||||||
|
// { type: "command", name: "ios-fork", args: "abc123" }
|
||||||
|
// Sidecar ruft pi-internen Command-Handler auf
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problem:** Geht nur in interactive mode, nicht in RPC/print mode. Außerdem muss die Extension selbst die Commands implementieren, was dupliziert Code.
|
||||||
|
|
||||||
|
#### Option C: Upstream-Feature-Request
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Gewünschte API:
|
||||||
|
pi.prompt(text: string): Promise<void>;
|
||||||
|
// oder
|
||||||
|
pi.executeCommand(commandName: string, args: string): Promise<void>;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fazit:** Gruppe T Tree-Navigation via **Slash-Command-Injection ist NICHT machbar** ohne Hack. Option A (`(pi as any)`) funktioniert vermutlich, ist aber fragil. Option B (Re-Implement) ist sauberer, aber aufwändiger. Option C (Upstream) ist langfristig richtig, aber blockiert die iOS-App.
|
||||||
|
|
||||||
|
**Empfehlung:** Für MVP: **Option B** (Re-Implement Commands in Extension). Für v2: **Upstream-Feature-Request für `pi.prompt()`**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.5 Tool-Call-Daten
|
||||||
|
|
||||||
|
**Was existiert:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Quelle: types.d.ts, Zeile 261-278
|
||||||
|
pi.on("tool_execution_start", async (event, ctx) => {
|
||||||
|
// event = {
|
||||||
|
// type: "tool_execution_start",
|
||||||
|
// toolCallId: string,
|
||||||
|
// toolName: string,
|
||||||
|
// args: any
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.on("tool_execution_end", async (event, ctx) => {
|
||||||
|
// event = {
|
||||||
|
// type: "tool_execution_end",
|
||||||
|
// toolCallId: string,
|
||||||
|
// toolName: string,
|
||||||
|
// result: any, // NICHT truncated!
|
||||||
|
// isError: boolean
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Code-Beleg (bestehende Extension nutzt das bereits):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// pi-remote-control/extensions/remote-control/index.ts, Zeile 103-127
|
||||||
|
pi.on("tool_execution_start", async (event) => {
|
||||||
|
server?.broadcast({
|
||||||
|
type: "tool_start",
|
||||||
|
toolCallId: event.toolCallId,
|
||||||
|
toolName: event.toolName,
|
||||||
|
args: event.args,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.on("tool_execution_end", async (event) => {
|
||||||
|
type TextContent = { type: string; text: string };
|
||||||
|
type ToolResult = { content?: TextContent[] } | string;
|
||||||
|
const result = event.result as ToolResult;
|
||||||
|
const content = typeof result === "object" ? result.content : undefined;
|
||||||
|
const resultText = Array.isArray(content)
|
||||||
|
? content
|
||||||
|
.filter((c) => c.type === "text")
|
||||||
|
.map((c) => c.text)
|
||||||
|
.join("")
|
||||||
|
: typeof result === "string"
|
||||||
|
? result
|
||||||
|
: "";
|
||||||
|
server?.broadcast({
|
||||||
|
type: "tool_end",
|
||||||
|
toolCallId: event.toolCallId,
|
||||||
|
result: resultText,
|
||||||
|
isError: event.isError,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fazit:** Tool-Call-Daten sind **vollständig verfügbar** für S-07. Name, Args, Result (ungekürzt), Error-Status — alles da. Trivial implementierbar.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.6 Sonstiges (Bonus)
|
||||||
|
|
||||||
|
**Weitere nützliche Capabilities, die wir ggf. übersehen haben:**
|
||||||
|
|
||||||
|
#### Model-Info
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Quelle: types.d.ts, Zeile 90
|
||||||
|
ctx.model: Model<any> | undefined;
|
||||||
|
|
||||||
|
// Beispiel:
|
||||||
|
pi.on("model_select", async (event, ctx) => {
|
||||||
|
broadcast({
|
||||||
|
type: "model",
|
||||||
|
model: {
|
||||||
|
id: event.model.id,
|
||||||
|
name: event.model.name,
|
||||||
|
provider: event.model.provider
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use-Case für iOS-App:** Session-Switcher (iOS-D-01) könnte pro Session anzeigen, welches Modell verwendet wurde.
|
||||||
|
|
||||||
|
#### CWD-Tracking
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Quelle: types.d.ts, Zeile 85
|
||||||
|
ctx.cwd: string;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use-Case:** Session-Metadaten (S-09) könnte CWD pro Session exposen.
|
||||||
|
|
||||||
|
#### Abort-Signal
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Quelle: types.d.ts, Zeile 95
|
||||||
|
ctx.abort(): void;
|
||||||
|
ctx.signal: AbortSignal | undefined;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use-Case:** iOS-App könnte einen "Stop"-Button haben, der `abort()` triggert via WebSocket-Control-Frame.
|
||||||
|
|
||||||
|
#### Context-Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Quelle: types.d.ts, Zeile 102
|
||||||
|
ctx.getContextUsage(): ContextUsage | undefined;
|
||||||
|
|
||||||
|
interface ContextUsage {
|
||||||
|
tokens: number | null;
|
||||||
|
contextWindow: number;
|
||||||
|
percent: number | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use-Case:** Status-Bar (iOS-C-01) könnte "Context: 45%" anzeigen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Empfehlung pro Spec-Feature
|
||||||
|
|
||||||
|
| Spec-Feature | Status | Empfehlung | Begründung |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **S-07 State Side-Channel** | ✅ Machbar | Implementieren | Events ausreichend granular, `isIdle()` + `agent_end` = awaiting-input |
|
||||||
|
| **S-08 Slash-Command-Registry** | ✅ Machbar | Implementieren | `pi.getCommands()` out-of-the-box |
|
||||||
|
| **S-13 Tree-State Side-Channel** | ⚠️ Machbar | Implementieren mit Events | Tree-Read trivial, Live-Updates via `session_tree` + `session_compact` |
|
||||||
|
| **Gruppe T Tree-Navigation** | ❌ Hack nötig | **Nicht empfohlen für MVP** | Slash-Command-Injection geht nicht. Option B (Re-Implement) aufwändig, Option A (Hack) fragil. Upstream-Request für v2. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Vorgeschlagene Upstream-Änderungen
|
||||||
|
|
||||||
|
### 5.1 `pi.prompt()` für Slash-Command-Dispatch
|
||||||
|
|
||||||
|
**Problem:** Extensions können User-Messages senden (`pi.sendUserMessage()`), aber keine Slash-Commands programmatisch dispatchen. Das limitiert iOS-App-Features wie Tree-Navigation (Gruppe T), weil `/fork`, `/new`, `/compact` nur als Text an den LLM gehen, nicht ausgeführt werden.
|
||||||
|
|
||||||
|
**Vorschlag:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
pi.prompt(text: string, options?: {
|
||||||
|
expandPromptTemplates?: boolean
|
||||||
|
}): Promise<void>;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Begründung:**
|
||||||
|
- Extensions die als "Remote-Control" fungieren (RPC, iOS-App, Web-UI) brauchen die Möglichkeit, Slash-Commands im Namen des Users auszuführen.
|
||||||
|
- Bestehende `pi.sendUserMessage()` umgeht absichtlich Template-Expansion (vermutlich für Safety). Ein explizites `pi.prompt()` signalisiert Intent.
|
||||||
|
- Alternative: `pi.executeCommand(name, args)` wäre noch expliziter, aber weniger flexibel.
|
||||||
|
|
||||||
|
**Implementierung (Skizze):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In extensions/wrapper.ts
|
||||||
|
api.prompt = async (text: string, options = {}) => {
|
||||||
|
runtime.assertActive();
|
||||||
|
const { expandPromptTemplates = true } = options;
|
||||||
|
// Ruft die interne session.prompt() auf, die Slash-Commands dispatched
|
||||||
|
await runtime.session.prompt(text, { expandPromptTemplates });
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact:** Niedrig. Nur eine neue Methode in der `ExtensionAPI`, delegiert an existierende `session.prompt()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.2 `on("awaiting_input")` Event
|
||||||
|
|
||||||
|
**Problem:** Um iOS-C-02 (Push-Notification "Pi ist fertig") sauber zu implementieren, brauchen wir ein explizites Event statt Ableitung via `agent_end` + `ctx.isIdle()`.
|
||||||
|
|
||||||
|
**Vorschlag:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
pi.on("awaiting_input", async (event, ctx) => {
|
||||||
|
// event = { type: "awaiting_input" }
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Begründung:**
|
||||||
|
- Explizit ist besser als implizit. `agent_end` + `isIdle()` funktioniert, ist aber umständlich und fehleranfällig (z.B. wenn Follow-Up-Messages pending sind).
|
||||||
|
- Semantisch klarer: "Pi wartet auf User-Input" ist ein State, kein abgeleitetes Konstrukt.
|
||||||
|
|
||||||
|
**Implementierung (Skizze):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In core/agent-session.ts, nach dem agent loop:
|
||||||
|
if (this.isIdle() && !this.hasPendingMessages()) {
|
||||||
|
await this.eventBus.emit("awaiting_input", {}, this.buildContext());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact:** Sehr niedrig. Ein neues Event, keine Breaking-Changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.3 Tree-Subscribe-Pattern (Optional)
|
||||||
|
|
||||||
|
**Problem:** `session_tree` + `session_compact` Events decken die meisten Tree-Mutations ab, aber nicht alle (z.B. neue Messages werden per `message_end` gemeldet, nicht als Tree-Mutation).
|
||||||
|
|
||||||
|
**Vorschlag:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
pi.on("tree_change", async (event, ctx) => {
|
||||||
|
// event = {
|
||||||
|
// type: "tree_change",
|
||||||
|
// operation: "append" | "branch" | "compact" | "fork",
|
||||||
|
// entryId: string,
|
||||||
|
// parentId: string | null
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Begründung:**
|
||||||
|
- Vereinheitlicht alle Tree-Mutations in einem Event.
|
||||||
|
- Extensions müssen nicht mehrere Events tracken (`message_end`, `session_tree`, `session_compact`).
|
||||||
|
|
||||||
|
**Implementierung:** Moderater Aufwand, weil es alle Append-Operationen tracken muss.
|
||||||
|
|
||||||
|
**Impact:** Niedrig, optional. Die bestehenden Events reichen aus, aber das wäre cleaner.
|
||||||
|
|
||||||
|
**Status:** **NICE-TO-HAVE**, nicht kritisch für die iOS-App.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Zusammenfassung: Implementierungs-Roadmap für Sidecar
|
||||||
|
|
||||||
|
### Phase 1: S-07 State Side-Channel (MUST)
|
||||||
|
|
||||||
|
**Code-Änderungen (pi-remote-control/extensions/remote-control/server.ts):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface StateMessage {
|
||||||
|
type: "state";
|
||||||
|
value: "thinking" | "tool" | "idle" | "awaiting-input";
|
||||||
|
tool?: string;
|
||||||
|
ts: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentState: StateMessage["value"] = "idle";
|
||||||
|
|
||||||
|
pi.on("agent_start", async () => {
|
||||||
|
currentState = "thinking";
|
||||||
|
server?.broadcast({
|
||||||
|
type: "state",
|
||||||
|
value: "thinking",
|
||||||
|
ts: Date.now()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.on("tool_execution_start", async (event) => {
|
||||||
|
currentState = "tool";
|
||||||
|
server?.broadcast({
|
||||||
|
type: "state",
|
||||||
|
value: "tool",
|
||||||
|
tool: event.toolName,
|
||||||
|
ts: Date.now()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.on("tool_execution_end", async () => {
|
||||||
|
currentState = "thinking";
|
||||||
|
server?.broadcast({
|
||||||
|
type: "state",
|
||||||
|
value: "thinking",
|
||||||
|
ts: Date.now()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.on("agent_end", async (event, ctx) => {
|
||||||
|
if (ctx.isIdle()) {
|
||||||
|
currentState = "awaiting-input";
|
||||||
|
server?.broadcast({
|
||||||
|
type: "state",
|
||||||
|
value: "awaiting-input",
|
||||||
|
ts: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Aufwand:** 1-2 Stunden
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: S-08 Slash-Command-Registry (SHOULD)
|
||||||
|
|
||||||
|
**Code-Änderungen (neuer Endpoint):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In server.ts, HTTP-Handler erweitern:
|
||||||
|
if (pathname === "/commands") {
|
||||||
|
const commands = pi.getCommands();
|
||||||
|
const payload = commands.map(c => ({
|
||||||
|
name: c.name,
|
||||||
|
description: c.description,
|
||||||
|
source: c.source
|
||||||
|
}));
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify(payload));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Aufwand:** 30 Minuten
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: S-13 Tree-State Side-Channel (SHOULD)
|
||||||
|
|
||||||
|
**Code-Änderungen (neuer Endpoint + Events):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Helper für Tree-Serialisierung
|
||||||
|
function serializeTree(ctx: ExtensionContext) {
|
||||||
|
const tree = ctx.sessionManager.getTree();
|
||||||
|
const leafId = ctx.sessionManager.getLeafId();
|
||||||
|
|
||||||
|
function mapNode(node: SessionTreeNode) {
|
||||||
|
let summary = "";
|
||||||
|
let msgCount = 0;
|
||||||
|
|
||||||
|
// Entry-Type-spezifische Logik
|
||||||
|
if (node.entry.type === "message") {
|
||||||
|
const msg = (node.entry as any).message;
|
||||||
|
if (msg.role === "user") {
|
||||||
|
const text = typeof msg.content === "string"
|
||||||
|
? msg.content
|
||||||
|
: msg.content.filter(c => c.type === "text").map(c => c.text).join("");
|
||||||
|
summary = text.slice(0, 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count messages recursive
|
||||||
|
const count = (n: SessionTreeNode): number => {
|
||||||
|
let c = n.entry.type === "message" ? 1 : 0;
|
||||||
|
for (const child of n.children) c += count(child);
|
||||||
|
return c;
|
||||||
|
};
|
||||||
|
msgCount = count(node);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: node.entry.id,
|
||||||
|
parent: node.entry.parentId,
|
||||||
|
summary,
|
||||||
|
messageCount: msgCount,
|
||||||
|
createdAt: node.entry.timestamp,
|
||||||
|
children: node.children.map(mapNode)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "tree",
|
||||||
|
nodes: tree.map(mapNode),
|
||||||
|
current: leafId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// REST-Endpoint
|
||||||
|
if (pathname === "/tree") {
|
||||||
|
const treeData = serializeTree(ctx);
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify(treeData));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live-Updates via Events
|
||||||
|
pi.on("session_tree", async (event, ctx) => {
|
||||||
|
const treeData = serializeTree(ctx);
|
||||||
|
server?.broadcast(treeData);
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.on("session_compact", async (event, ctx) => {
|
||||||
|
const treeData = serializeTree(ctx);
|
||||||
|
server?.broadcast(treeData);
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.on("message_end", async (event, ctx) => {
|
||||||
|
// Neue Message = Tree-Mutation
|
||||||
|
const treeData = serializeTree(ctx);
|
||||||
|
server?.broadcast(treeData);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Aufwand:** 2-3 Stunden
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 4: Gruppe T — Tree-Navigation (CONDITIONAL)
|
||||||
|
|
||||||
|
**Ohne Upstream-Change: Option B (Re-Implement)**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
pi.registerCommand("ios-fork", {
|
||||||
|
description: "Fork from iOS app (internal)",
|
||||||
|
handler: async (args, ctx) => {
|
||||||
|
const entryId = args.trim();
|
||||||
|
await ctx.fork(entryId, { position: "at" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.registerCommand("ios-new", {
|
||||||
|
description: "New session from iOS app (internal)",
|
||||||
|
handler: async (args, ctx) => {
|
||||||
|
await ctx.newSession();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.registerCommand("ios-compact", {
|
||||||
|
description: "Compact from iOS app (internal)",
|
||||||
|
handler: async (args, ctx) => {
|
||||||
|
ctx.compact();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// WebSocket-Handler für iOS-Command-Requests
|
||||||
|
ws.on("message", (data) => {
|
||||||
|
const msg = JSON.parse(data.toString());
|
||||||
|
if (msg.type === "execute_command") {
|
||||||
|
// iOS schickt: { type: "execute_command", name: "ios-fork", args: "abc123" }
|
||||||
|
const command = registeredCommands.get(msg.name);
|
||||||
|
if (command) {
|
||||||
|
command.handler(msg.args, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Aufwand:** 3-4 Stunden (inkl. Testing)
|
||||||
|
|
||||||
|
**Alternative mit Upstream-Change:**
|
||||||
|
|
||||||
|
Sobald `pi.prompt()` verfügbar:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
ws.on("message", (data) => {
|
||||||
|
const msg = JSON.parse(data.toString());
|
||||||
|
if (msg.type === "slash_command") {
|
||||||
|
// iOS schickt: { type: "slash_command", text: "/fork abc123" }
|
||||||
|
await pi.prompt(msg.text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Aufwand:** 30 Minuten
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Risiken & Offene Punkte
|
||||||
|
|
||||||
|
### R-1: Timing-Race-Condition bei State-Ableitung
|
||||||
|
|
||||||
|
**Problem:** `agent_end` + `ctx.isIdle()` könnte in Randfällen falsch sein (z.B. wenn zwischen Event-Emit und `isIdle()`-Check ein Follow-Up-Message queued wurde).
|
||||||
|
|
||||||
|
**Mitigation:** Upstream-Event `awaiting_input` (siehe 5.2) wäre sauberer. Für MVP akzeptabel, für Production ggf. nachbessern.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### R-2: Tree-Serialisierung bei großen Sessions
|
||||||
|
|
||||||
|
**Problem:** Bei sehr langen Sessions (> 1000 Entries) kann `getTree()` + Serialisierung langsam werden.
|
||||||
|
|
||||||
|
**Mitigation:**
|
||||||
|
- Lazy-Loading: iOS-App ruft nur Subtrees ab (via `ctx.sessionManager.getChildren(parentId)`).
|
||||||
|
- Caching: Sidecar cached serialisiertes Tree und invalidiert nur bei Tree-Events.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### R-3: Slash-Command-Hack (`(pi as any)`) bricht bei Updates
|
||||||
|
|
||||||
|
**Problem:** Wenn wir Option A (Hack) für Gruppe T nutzen, kann das bei pi-Updates brechen.
|
||||||
|
|
||||||
|
**Mitigation:**
|
||||||
|
- Tests einbauen die warnen wenn der Hack bricht.
|
||||||
|
- Upstream-Feature-Request parallel verfolgen.
|
||||||
|
- Für MVP Option B (Re-Implement) bevorzugen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### R-4: ExtensionAPI-Version-Lock
|
||||||
|
|
||||||
|
**Problem:** Wir auditieren gegen pi v0.74.0. Künftige Versionen könnten API-Changes haben.
|
||||||
|
|
||||||
|
**Mitigation:**
|
||||||
|
- In `package.json` der Extension `peerDependencies` festlegen: `"@earendil-works/pi-coding-agent": "^0.74.0"`.
|
||||||
|
- Bei pi-Updates: Re-Audit durchführen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Fazit & Next Steps
|
||||||
|
|
||||||
|
**Für MVP (Phase 0):**
|
||||||
|
- ✅ S-07, S-08, S-13 sind **implementierbar ohne Upstream-Changes**
|
||||||
|
- ⚠️ Gruppe T (Tree-Navigation) **nicht empfohlen für MVP** (Hack fragil, Re-Implement aufwändig)
|
||||||
|
- → **Empfehlung:** MVP ohne Gruppe T. Tree-View als read-only (S-13), Navigation bleibt über SSH-Terminal.
|
||||||
|
|
||||||
|
**Für v2 (nach Upstream-Discussion):**
|
||||||
|
- Feature-Request für `pi.prompt()` stellen (siehe 5.1)
|
||||||
|
- Optional: `awaiting_input` Event (siehe 5.2)
|
||||||
|
- Gruppe T dann sauber implementieren
|
||||||
|
|
||||||
|
**Nächste Schritte:**
|
||||||
|
1. Spec v3 schreiben mit finalen Feature-Entscheidungen
|
||||||
|
2. Phase 0 (Stream-PoC) starten — unabhängig von diesem Audit
|
||||||
|
3. Phase 1 (Sidecar mit S-07, S-08, S-13) parallel entwickeln
|
||||||
|
4. iOS-App-Entwicklung kann starten sobald Phase 1 läuft
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Audit abgeschlossen.**
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
# pi-remote — iOS Native App Spec (v2)
|
# pi-remote — iOS Native App Spec (v3)
|
||||||
|
|
||||||
> **Status:** v2 nach Review-Runde 1.
|
> **Status:** v3 nach ExtensionAPI-Audit.
|
||||||
> Review-Verlauf mit allen Diskussionen: [`SPEC-ios-app-review-v1.md`](./SPEC-ios-app-review-v1.md).
|
> Audit-Ergebnis: [`EXTENSION-API-AUDIT.md`](./EXTENSION-API-AUDIT.md).
|
||||||
> Vor Phase 0 ist ein API-Audit nötig — siehe [Spike-0a](#spike-0a--extensionapi-audit).
|
> Review-Verlauf v1: [`SPEC-ios-app-review-v1.md`](./SPEC-ios-app-review-v1.md).
|
||||||
|
>
|
||||||
|
> **Changelog v2 → v3:**
|
||||||
|
> - Audit hat S-07, S-08 und Tree-Read als out-of-the-box machbar bestätigt → von PENDING auf firm SHOULD.
|
||||||
|
> - **Tree-Navigation komplett aus iOS entfernt.** Gruppe T gestrichen. Begründung: Slash-Command-Dispatch ist in der ExtensionAPI blockiert, Workarounds (Hack oder Re-Implement) sind nicht den Aufwand wert. Tree-Navigation bleibt nativ in pi.
|
||||||
|
> - Spike-0a abgeschlossen, ist nun referenziertes Audit-Dokument.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -170,24 +175,25 @@ einzelne Datei `/var/lib/pi-remote/buffer/<session>.log`.
|
||||||
*Dependencies:* S-02
|
*Dependencies:* S-02
|
||||||
|
|
||||||
### S-07 — State Side-Channel
|
### S-07 — State Side-Channel
|
||||||
**SHOULD.** Sidecar abonniert pi-ExtensionAPI-Events und publiziert als
|
**SHOULD.** Sidecar abonniert pi-ExtensionAPI-Events (`agent_start`,
|
||||||
JSON-Control-Frames:
|
`agent_end`, `tool_execution_start`, `tool_execution_end`,
|
||||||
|
`session_tree`, `session_compact`) und publiziert als JSON-Control-Frames:
|
||||||
```json
|
```json
|
||||||
{"type":"state","value":"thinking"|"tool"|"idle"|"awaiting-input",
|
{"type":"state","value":"thinking"|"tool"|"idle"|"awaiting-input",
|
||||||
"tool":"Edit"|"Read"|...,"ts":1234567890}
|
"tool":"Edit"|"Read"|...,"ts":1234567890}
|
||||||
```
|
```
|
||||||
Realisierbarkeit hängt vom Outcome von [Spike-0a](#spike-0a--extensionapi-audit) ab.
|
`awaiting-input` wird abgeleitet aus `agent_end` + `ctx.isIdle()`,
|
||||||
|
da kein explizites Event existiert (siehe Audit §3.1).
|
||||||
|
|
||||||
*Dependencies:* S-01, Spike-0a
|
*Dependencies:* S-01
|
||||||
|
|
||||||
### S-08 — Slash-Command-Registry
|
### S-08 — Slash-Command-Registry
|
||||||
**SHOULD.** Endpoint `/sessions/<id>/commands` liefert JSON-Liste der
|
**SHOULD.** Endpoint `/sessions/<id>/commands` liefert JSON-Liste der
|
||||||
verfügbaren Slash-Commands inkl. Beschreibung und Argument-Schema, aus
|
verfügbaren Slash-Commands inkl. Beschreibung und Argument-Schema via
|
||||||
pi's Registry abgefragt. Dynamisch — Extensions die Commands
|
`pi.getCommands()` (laut Audit out-of-the-box verfügbar). Dynamisch —
|
||||||
hinzufügen erscheinen automatisch. Realisierbarkeit hängt von Spike-0a
|
Extensions die Commands hinzufügen erscheinen automatisch.
|
||||||
ab.
|
|
||||||
|
|
||||||
*Dependencies:* S-01, Spike-0a
|
*Dependencies:* S-01
|
||||||
|
|
||||||
### S-09 — Multi-Session-Management
|
### S-09 — Multi-Session-Management
|
||||||
**MUST.** Sidecar verwaltet mehrere tmux-Sessions parallel, alle robust
|
**MUST.** Sidecar verwaltet mehrere tmux-Sessions parallel, alle robust
|
||||||
|
|
@ -263,6 +269,9 @@ Selbst-Debugging.
|
||||||
|
|
||||||
*Dependencies:* —
|
*Dependencies:* —
|
||||||
|
|
||||||
|
> S-13 (Tree-State Side-Channel) wurde in v3 gestrichen.
|
||||||
|
> Begründung: siehe Out-of-Scope und Changelog.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. iOS Client Features
|
## 5. iOS Client Features
|
||||||
|
|
@ -509,43 +518,6 @@ künftige Connection verifiziert dagegen. Re-Pairing bei Cert-Rotation.
|
||||||
|
|
||||||
*Dependencies:* S-10, S-11
|
*Dependencies:* S-10, S-11
|
||||||
|
|
||||||
### Gruppe T — Tree Navigation (PENDING)
|
|
||||||
|
|
||||||
> **Status:** Pending — abhängig vom Outcome von
|
|
||||||
> [Spike-0a](#spike-0a--extensionapi-audit). Wenn pi's Tree über die
|
|
||||||
> ExtensionAPI nicht zugänglich ist, fällt diese Gruppe weg oder wird auf
|
|
||||||
> reine Slash-Command-Injection reduziert (`/fork`, `/new`, `/compact`).
|
|
||||||
|
|
||||||
#### S-13 (NEU) — Tree-State Side-Channel
|
|
||||||
**TBD.** Sidecar liefert pi's Conversation-Tree als JSON über
|
|
||||||
Side-Channel + Live-Updates bei Branch/Fork/Compact/Checkout:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "tree",
|
|
||||||
"nodes": [
|
|
||||||
{"id":"abc","parent":null,"summary":"main","msgCount":42,"createdAt":"..."},
|
|
||||||
{"id":"def","parent":"abc","summary":"explored iOS","msgCount":18}
|
|
||||||
],
|
|
||||||
"current": "def"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
*Dependencies:* Spike-0a
|
|
||||||
|
|
||||||
#### iOS-T-01 — Native Tree-View
|
|
||||||
**TBD.** SwiftUI-Tree-Sheet mit allen Knoten. Tap auf Knoten →
|
|
||||||
Checkout via Slash-Command-Injection. Aktueller Knoten visuell
|
|
||||||
hervorgehoben.
|
|
||||||
|
|
||||||
*Dependencies:* S-13
|
|
||||||
|
|
||||||
#### iOS-T-02 — Branch / Fork Actions
|
|
||||||
**TBD.** Aus dem Tree-Sheet heraus `/fork`, `/new`, `/compact` als
|
|
||||||
native Buttons. Werden via Slash-Command-Injection an pi gesendet.
|
|
||||||
|
|
||||||
*Dependencies:* S-13
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Out of Scope (locked)
|
## 6. Out of Scope (locked)
|
||||||
|
|
@ -568,38 +540,26 @@ native Buttons. Werden via Slash-Command-Injection an pi gesendet.
|
||||||
Snapshot funktioniert weiter.
|
Snapshot funktioniert weiter.
|
||||||
- **Bookmarks / manuelle Marker.** Pi-interne Tools sollen das übernehmen.
|
- **Bookmarks / manuelle Marker.** Pi-interne Tools sollen das übernehmen.
|
||||||
- **Snippet-Library** (Prompts mit Variablen). Pi-interne Tools.
|
- **Snippet-Library** (Prompts mit Variablen). Pi-interne Tools.
|
||||||
|
- **Tree-View / Tree-Navigation in iOS** (jeglicher Form, read-only
|
||||||
|
oder interaktiv). Tree wird ausschließlich nativ in pi bedient. Audit
|
||||||
|
hat gezeigt: Slash-Command-Dispatch ist in der ExtensionAPI nicht sauber
|
||||||
|
zugänglich; Workarounds sind die Komplexität nicht wert.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Spike-0a — ExtensionAPI-Audit
|
## 7. ExtensionAPI-Audit (abgeschlossen)
|
||||||
|
|
||||||
**Status:** Muss vor Phase 0 abgeschlossen sein.
|
Das Audit wurde durchgeführt; Ergebnis liegt in
|
||||||
|
[`EXTENSION-API-AUDIT.md`](./EXTENSION-API-AUDIT.md).
|
||||||
|
|
||||||
**Ziel:** Klären, welche der Features S-07, S-08, S-13 sowie Gruppe T
|
**Kernergebnisse:**
|
||||||
mit der heutigen pi-ExtensionAPI realisierbar sind und welche Upstream-
|
- S-07, S-08, sowie Tree-**Read** sind out-of-the-box machbar.
|
||||||
Erweiterungen wir brauchen.
|
- Tree-**Write** (Slash-Command-Dispatch wie `/fork`, `/checkout`,
|
||||||
|
`/new`) ist nicht sauber zugänglich → Gruppe T gestrichen (siehe
|
||||||
|
Out-of-Scope).
|
||||||
|
- Tool-Call-Daten sind vollständig verfügbar (Name, Args, Result).
|
||||||
|
|
||||||
**Output:** Markdown-Dokument `docs/EXTENSION-API-AUDIT.md`, das für
|
Kein weiterer Spike vor Phase 1 nötig.
|
||||||
jede ExtensionAPI-Capability dokumentiert:
|
|
||||||
- API-Signatur
|
|
||||||
- Welche Events / Daten exposed sind
|
|
||||||
- Welches Spec-Feature darauf basiert
|
|
||||||
- Workaround falls API fehlt (`(pi as any)`, Slash-Injection, etc.)
|
|
||||||
- Falls Upstream-Change nötig: konkreter Vorschlag
|
|
||||||
|
|
||||||
**Konkrete Fragen:**
|
|
||||||
- Welche Lifecycle-Events liefert die ExtensionAPI?
|
|
||||||
(`thinking-start/end`, `tool-start/end`, `awaiting-input`, ...)
|
|
||||||
- Ist die Slash-Command-Registry abrufbar? In welcher Form?
|
|
||||||
- Wie ist der Conversation-Tree intern repräsentiert? Read-Access?
|
|
||||||
Subscribe? Mutations?
|
|
||||||
- Gibt es einen `pi.prompt(text)` oder Äquivalent, das Slash-Commands
|
|
||||||
korrekt dispatched?
|
|
||||||
- Welche Tool-Call-Daten sind sichtbar (Tool-Name, Argumente,
|
|
||||||
Ergebnis)?
|
|
||||||
|
|
||||||
**Nicht-Blockend:** Stream-Path (S-01–S-06) und Input (S-03) sind vom
|
|
||||||
Audit unabhängig — die können parallel oder vorab beginnen.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -610,25 +570,20 @@ Audit unabhängig — die können parallel oder vorab beginnen.
|
||||||
- **Spike-0 — Stream-PoC** — `pi-remote-control` um tmux pipe-pane +
|
- **Spike-0 — Stream-PoC** — `pi-remote-control` um tmux pipe-pane +
|
||||||
WS-Stream erweitern. Verifizieren dass pi sauber in tmux läuft (kein
|
WS-Stream erweitern. Verifizieren dass pi sauber in tmux läuft (kein
|
||||||
Crash, Alternate-Screen sauber, kein Latency-Problem).
|
Crash, Alternate-Screen sauber, kein Latency-Problem).
|
||||||
- **Spike-0a — ExtensionAPI-Audit** — siehe oben.
|
- **Phase 1 — Sidecar production-ready** — S-01 bis S-12 (alle), S-09a
|
||||||
- **Phase 1 — Sidecar production-ready** — S-01 bis S-06, S-09, S-10,
|
optional.
|
||||||
S-11. Optional je nach Audit: S-07, S-08, S-13.
|
|
||||||
- **Phase 2 — iOS-App MVP** — Renderer (Gruppe A), Input (Gruppe B
|
- **Phase 2 — iOS-App MVP** — Renderer (Gruppe A), Input (Gruppe B
|
||||||
ohne Hardware-KB), Status-Bar (iOS-C-01), Session-Switcher
|
ohne Hardware-KB), Status-Bar (iOS-C-01), Session-Switcher
|
||||||
(iOS-D-01 + a/b), Reconnect-Lifecycle (Gruppe E), Auth (Gruppe F).
|
(iOS-D-01 + a/b), Reconnect-Lifecycle (Gruppe E), Auth (Gruppe F).
|
||||||
- **Phase 3 — iOS-App Augmentation** — Slash-Palette (iOS-C-04),
|
- **Phase 3 — iOS-App Augmentation** — Slash-Palette (iOS-C-04),
|
||||||
Voice (iOS-C-05), Thumbnails (iOS-D-01c), Scrollback-Search
|
Voice (iOS-C-05), Thumbnails (iOS-D-01c), Scrollback-Search
|
||||||
(iOS-D-02), Hardware-KB (iOS-B-06).
|
(iOS-D-02), Hardware-KB (iOS-B-06).
|
||||||
- **Phase 4 — Tree (conditional)** — Gruppe T, sofern Spike-0a
|
|
||||||
positiv.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. Offene Punkte für v3
|
## 9. Offene Punkte für v4
|
||||||
|
|
||||||
- **Q-A** — pi-CLI one-shot mode für S-09a: CLI-Flags müssen verifiziert
|
- **Q-A** — pi-CLI one-shot mode für S-09a: CLI-Flags müssen verifiziert
|
||||||
werden. Existiert das in der heutigen pi-CLI?
|
werden. Existiert das in der heutigen pi-CLI?
|
||||||
- **Q-B** — Wenn Spike-0a zeigt dass S-07/S-08 nicht ohne Hack möglich:
|
|
||||||
Hack akzeptieren (`(pi as any)`) oder Upstream-Change pushen?
|
|
||||||
- **Q-C** — APNs-Setup Details (Auth-Key-Provisioning, Token-Lifecycle).
|
- **Q-C** — APNs-Setup Details (Auth-Key-Provisioning, Token-Lifecycle).
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue