@schilderlabs/pitown-package 0.2.1

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.
@@ -0,0 +1,309 @@
1
+ import { Type } from "@mariozechner/pi-ai"
2
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent"
3
+ import {
4
+ delegateTask,
5
+ listAgentStates,
6
+ queueAgentMessage,
7
+ readAgentMessages,
8
+ readAgentState,
9
+ updateAgentStatus,
10
+ } from "@schilderlabs/pitown-core"
11
+ import { readPiTownMayorPrompt, resolvePiTownExtensionPath } from "#pitown-package-api"
12
+
13
+ type TownToolName =
14
+ | "pitown_board"
15
+ | "pitown_delegate"
16
+ | "pitown_message_agent"
17
+ | "pitown_peek_agent"
18
+ | "pitown_update_status"
19
+
20
+ interface TownAgentContext {
21
+ artifactsDir: string
22
+ repoSlug: string
23
+ agentId: string
24
+ role: string
25
+ sessionFile: string
26
+ }
27
+
28
+ const ROLE_STATUS = Type.Union([
29
+ Type.Literal("queued"),
30
+ Type.Literal("running"),
31
+ Type.Literal("idle"),
32
+ Type.Literal("blocked"),
33
+ Type.Literal("completed"),
34
+ Type.Literal("failed"),
35
+ ])
36
+
37
+ const toolPermissions: Record<string, TownToolName[]> = {
38
+ leader: ["pitown_board", "pitown_delegate", "pitown_message_agent", "pitown_peek_agent", "pitown_update_status"],
39
+ worker: ["pitown_board", "pitown_message_agent", "pitown_peek_agent", "pitown_update_status"],
40
+ reviewer: ["pitown_board", "pitown_message_agent", "pitown_peek_agent", "pitown_update_status"],
41
+ "docs-keeper": ["pitown_board", "pitown_message_agent", "pitown_peek_agent", "pitown_update_status"],
42
+ }
43
+
44
+ export function resolveTownAgentContext(sessionFile: string | null | undefined): TownAgentContext | null {
45
+ if (!sessionFile) return null
46
+
47
+ const normalizedSessionFile = sessionFile.replace(/\\/g, "/")
48
+ const match = normalizedSessionFile.match(/^(.*\/repos\/([^/]+))\/agents\/([^/]+)\/sessions\/[^/]+\.jsonl$/)
49
+ if (!match) return null
50
+
51
+ const artifactsDir = match[1]
52
+ const repoSlug = match[2]
53
+ const agentId = match[3]
54
+ if (!artifactsDir || !repoSlug || !agentId) return null
55
+
56
+ const state = readAgentState(artifactsDir, agentId)
57
+ if (state === null) return null
58
+
59
+ return {
60
+ artifactsDir,
61
+ repoSlug,
62
+ agentId,
63
+ role: state.role,
64
+ sessionFile: normalizedSessionFile,
65
+ }
66
+ }
67
+
68
+ function getAllowedTools(role: string): TownToolName[] {
69
+ return toolPermissions[role] ?? ["pitown_board"]
70
+ }
71
+
72
+ function assertPermission(context: TownAgentContext, toolName: TownToolName, targetAgentId?: string) {
73
+ if (!getAllowedTools(context.role).includes(toolName)) {
74
+ throw new Error(`${context.role} agents may not use ${toolName}`)
75
+ }
76
+
77
+ if (toolName === "pitown_delegate" && context.role !== "leader") {
78
+ throw new Error("Only the leader may delegate work")
79
+ }
80
+
81
+ if (toolName === "pitown_message_agent" && context.role !== "leader" && targetAgentId && targetAgentId !== "leader") {
82
+ throw new Error("Only the leader may message non-leader agents")
83
+ }
84
+
85
+ if (toolName === "pitown_peek_agent" && context.role !== "leader" && targetAgentId) {
86
+ const allowedTargets = new Set([context.agentId, "leader"])
87
+ if (!allowedTargets.has(targetAgentId)) {
88
+ throw new Error("Non-leader agents may only peek themselves or the leader")
89
+ }
90
+ }
91
+ }
92
+
93
+ function formatBoard(artifactsDir: string): string {
94
+ const agents = listAgentStates(artifactsDir)
95
+ if (agents.length === 0) return "No agents found."
96
+
97
+ return agents
98
+ .map((agent) => {
99
+ const task = agent.task ?? "no active task"
100
+ const taskId = agent.taskId ? ` [${agent.taskId}]` : ""
101
+ const note = agent.lastMessage ? ` | ${agent.lastMessage}` : ""
102
+ const waitingOn = agent.waitingOn ? ` | waiting on: ${agent.waitingOn}` : ""
103
+ return `${agent.agentId.padEnd(12)} ${agent.status.padEnd(9)} ${task}${taskId}${note}${waitingOn}`
104
+ })
105
+ .join("\n")
106
+ }
107
+
108
+ function formatMailbox(artifactsDir: string, agentId: string, box: "inbox" | "outbox"): string {
109
+ const records = readAgentMessages(artifactsDir, agentId, box).slice(-3)
110
+ if (records.length === 0) return `${box}: empty`
111
+
112
+ return `${box}:\n${records.map((record) => `- ${record.from}: ${record.body}`).join("\n")}`
113
+ }
114
+
115
+ function buildStartupContext(context: TownAgentContext): string {
116
+ const state = readAgentState(context.artifactsDir, context.agentId)
117
+ if (state === null) throw new Error(`Unknown agent: ${context.agentId}`)
118
+
119
+ const currentTask = state.task ?? "no active task"
120
+ const board = formatBoard(context.artifactsDir)
121
+ const tools = getAllowedTools(context.role).join(", ")
122
+
123
+ return [
124
+ `Pi Town agent context: ${context.agentId} (${context.role})`,
125
+ `Repo slug: ${context.repoSlug}`,
126
+ `Current task: ${currentTask}`,
127
+ `Allowed Pi Town tools: ${tools}`,
128
+ "",
129
+ "Current board:",
130
+ board,
131
+ ].join("\n")
132
+ }
133
+
134
+ function requireTownContext(ctx: ExtensionContext): TownAgentContext {
135
+ const context = resolveTownAgentContext(ctx.sessionManager.getSessionFile())
136
+ if (context === null) {
137
+ throw new Error("This Pi session is not managed by Pi Town")
138
+ }
139
+ return context
140
+ }
141
+
142
+ export function registerTownTools(pi: ExtensionAPI) {
143
+ pi.on("before_agent_start", async (_event, ctx) => {
144
+ const context = resolveTownAgentContext(ctx.sessionManager.getSessionFile())
145
+ if (context === null) return undefined
146
+
147
+ return {
148
+ message: {
149
+ customType: "pitown-context",
150
+ content: buildStartupContext(context),
151
+ display: false,
152
+ },
153
+ }
154
+ })
155
+
156
+ pi.registerTool({
157
+ name: "pitown_board",
158
+ label: "Pi Town Board",
159
+ description: "Show the current Pi Town board with all known agents and their statuses",
160
+ parameters: Type.Object({}),
161
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
162
+ const context = requireTownContext(ctx)
163
+ assertPermission(context, "pitown_board")
164
+
165
+ return {
166
+ content: [{ type: "text", text: formatBoard(context.artifactsDir) }],
167
+ details: { agentId: context.agentId, role: context.role },
168
+ }
169
+ },
170
+ })
171
+
172
+ pi.registerTool({
173
+ name: "pitown_delegate",
174
+ label: "Pi Town Delegate",
175
+ description: "Delegate a bounded task to a new Pi Town agent",
176
+ parameters: Type.Object({
177
+ role: Type.String({ description: "Agent role to spawn, such as worker or reviewer" }),
178
+ task: Type.String({ description: "Bounded task description" }),
179
+ agentId: Type.Optional(Type.String({ description: "Optional explicit agent id" })),
180
+ }),
181
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
182
+ const context = requireTownContext(ctx)
183
+ assertPermission(context, "pitown_delegate")
184
+ const input = params as { role: string; task: string; agentId?: string }
185
+
186
+ const result = delegateTask({
187
+ repoRoot: ctx.cwd,
188
+ artifactsDir: context.artifactsDir,
189
+ fromAgentId: context.agentId,
190
+ role: input.role,
191
+ task: input.task,
192
+ agentId: input.agentId ?? null,
193
+ extensionPath: resolvePiTownExtensionPath(),
194
+ appendedSystemPrompt: input.role === "leader" ? readPiTownMayorPrompt() : null,
195
+ })
196
+
197
+ return {
198
+ content: [
199
+ {
200
+ type: "text",
201
+ text: `Delegated ${result.task.taskId} to ${result.agentId} (${input.role}). Exit code: ${result.piResult.exitCode}.`,
202
+ },
203
+ ],
204
+ details: {
205
+ taskId: result.task.taskId,
206
+ agentId: result.agentId,
207
+ status: result.task.status,
208
+ sessionPath: result.latestSession.sessionPath,
209
+ },
210
+ }
211
+ },
212
+ })
213
+
214
+ pi.registerTool({
215
+ name: "pitown_message_agent",
216
+ label: "Pi Town Message Agent",
217
+ description: "Send a durable message to another Pi Town agent",
218
+ parameters: Type.Object({
219
+ agentId: Type.String({ description: "Target Pi Town agent id" }),
220
+ body: Type.String({ description: "Message body" }),
221
+ }),
222
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
223
+ const context = requireTownContext(ctx)
224
+ const input = params as { agentId: string; body: string }
225
+ assertPermission(context, "pitown_message_agent", input.agentId)
226
+
227
+ queueAgentMessage({
228
+ artifactsDir: context.artifactsDir,
229
+ agentId: input.agentId,
230
+ from: context.agentId,
231
+ body: input.body,
232
+ })
233
+
234
+ return {
235
+ content: [{ type: "text", text: `Queued message for ${input.agentId}.` }],
236
+ details: { agentId: input.agentId },
237
+ }
238
+ },
239
+ })
240
+
241
+ pi.registerTool({
242
+ name: "pitown_peek_agent",
243
+ label: "Pi Town Peek Agent",
244
+ description: "Inspect another Pi Town agent's current state and recent mailbox activity",
245
+ parameters: Type.Object({
246
+ agentId: Type.String({ description: "Agent id to inspect" }),
247
+ }),
248
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
249
+ const context = requireTownContext(ctx)
250
+ const input = params as { agentId: string }
251
+ assertPermission(context, "pitown_peek_agent", input.agentId)
252
+
253
+ const state = readAgentState(context.artifactsDir, input.agentId)
254
+ if (state === null) throw new Error(`Unknown agent: ${input.agentId}`)
255
+
256
+ const summary = [
257
+ `${state.agentId} (${state.role})`,
258
+ `status: ${state.status}`,
259
+ `task: ${state.task ?? "none"}`,
260
+ `taskId: ${state.taskId ?? "none"}`,
261
+ `last message: ${state.lastMessage ?? "none"}`,
262
+ `waiting on: ${state.waitingOn ?? "none"}`,
263
+ formatMailbox(context.artifactsDir, input.agentId, "inbox"),
264
+ formatMailbox(context.artifactsDir, input.agentId, "outbox"),
265
+ ].join("\n")
266
+
267
+ return {
268
+ content: [{ type: "text", text: summary }],
269
+ details: { agentId: state.agentId, status: state.status, taskId: state.taskId },
270
+ }
271
+ },
272
+ })
273
+
274
+ pi.registerTool({
275
+ name: "pitown_update_status",
276
+ label: "Pi Town Update Status",
277
+ description: "Update this Pi Town agent's durable status on the board",
278
+ parameters: Type.Object({
279
+ status: ROLE_STATUS,
280
+ lastMessage: Type.Optional(Type.String({ description: "Short progress update" })),
281
+ waitingOn: Type.Optional(Type.String({ description: "What this agent is waiting on, if anything" })),
282
+ blocked: Type.Optional(Type.Boolean({ description: "Whether the agent is currently blocked" })),
283
+ }),
284
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
285
+ const context = requireTownContext(ctx)
286
+ assertPermission(context, "pitown_update_status")
287
+ const input = params as {
288
+ status: "queued" | "running" | "idle" | "blocked" | "completed" | "failed"
289
+ lastMessage?: string
290
+ waitingOn?: string
291
+ blocked?: boolean
292
+ }
293
+
294
+ updateAgentStatus({
295
+ artifactsDir: context.artifactsDir,
296
+ agentId: context.agentId,
297
+ status: input.status,
298
+ lastMessage: input.lastMessage ?? null,
299
+ waitingOn: input.waitingOn ?? null,
300
+ blocked: input.blocked,
301
+ })
302
+
303
+ return {
304
+ content: [{ type: "text", text: `Updated ${context.agentId} to ${input.status}.` }],
305
+ details: { agentId: context.agentId, status: input.status },
306
+ }
307
+ },
308
+ })
309
+ }
@@ -0,0 +1,8 @@
1
+ Start a Pi Town overnight run.
2
+
3
+ Goals:
4
+ - choose one bounded task
5
+ - keep context explicit and minimal
6
+ - prefer scout -> planner -> worker -> reviewer
7
+ - record blockers instead of stalling forever
8
+ - optimize for reviewed progress, not raw output volume
@@ -0,0 +1,8 @@
1
+ Retry the current task with the newest available context.
2
+
3
+ Checklist:
4
+ 1. restate the task briefly
5
+ 2. identify what blocked the prior attempt
6
+ 3. narrow the scope if needed
7
+ 4. prefer structural fixes over repeated blind retries
8
+ 5. stop and record a blocker if human input is truly required