pipeline-moe 0.1.0
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.
Potentially problematic release.
This version of pipeline-moe might be problematic. Click here for more details.
- package/.env.example +26 -0
- package/LICENSE +21 -0
- package/README.md +408 -0
- package/bin/pipeline-moe.mjs +40 -0
- package/package.json +68 -0
- package/presets/2106BUILD.json +163 -0
- package/presets/CHEAPBUILD.json +97 -0
- package/presets/FREEROOM.json +96 -0
- package/presets/Versa.json +145 -0
- package/presets/cloud-main.json +76 -0
- package/presets/cloud-sprint.json +70 -0
- package/presets/local-default.json +152 -0
- package/presets/main.json +147 -0
- package/presets/mainmix.json +145 -0
- package/src/circuit-breaker.ts +141 -0
- package/src/config.ts +46 -0
- package/src/custom-tools/arxiv-search.ts +186 -0
- package/src/custom-tools/check-room.ts +45 -0
- package/src/custom-tools/destroy-room.ts +45 -0
- package/src/custom-tools/index.ts +75 -0
- package/src/custom-tools/spawn-room.ts +99 -0
- package/src/custom-tools/stop-room.ts +50 -0
- package/src/custom-tools/web-read.ts +104 -0
- package/src/custom-tools/web-search.ts +144 -0
- package/src/custom-tools/youcom-search.ts +230 -0
- package/src/custom-tools/youtube-transcript.ts +124 -0
- package/src/local-model-lock.ts +52 -0
- package/src/model.ts +131 -0
- package/src/orchestrator.ts +59 -0
- package/src/participant.ts +423 -0
- package/src/path-guard.ts +13 -0
- package/src/personas.ts +480 -0
- package/src/receipts.ts +120 -0
- package/src/registry.ts +317 -0
- package/src/room-manager.ts +505 -0
- package/src/room.ts +1657 -0
- package/src/sandbox-tools.ts +141 -0
- package/src/server.ts +1846 -0
- package/src/sse.ts +86 -0
- package/src/sshfs.ts +139 -0
- package/src/store.ts +79 -0
- package/src/types.ts +131 -0
- package/src/validation.ts +36 -0
- package/tsconfig.json +15 -0
- package/web/dist/assets/index-CmMGhMKG.css +1 -0
- package/web/dist/assets/index-LAGqbZII.js +41 -0
- package/web/dist/index.html +13 -0
- package/web/tsconfig.json +21 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// arxiv_search — arXiv academic paper search tool definition
|
|
2
|
+
// Free, no API key. Returns paper titles, authors, abstracts, categories,
|
|
3
|
+
// and PDF links from the arXiv Atom feed API.
|
|
4
|
+
|
|
5
|
+
import { Type } from "typebox"
|
|
6
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent"
|
|
7
|
+
import type { AgentToolResult } from "@earendil-works/pi-coding-agent"
|
|
8
|
+
|
|
9
|
+
// Minimal text content type.
|
|
10
|
+
interface TextContent {
|
|
11
|
+
type: "text"
|
|
12
|
+
text: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const arxivSearchSchema = Type.Object({
|
|
16
|
+
query: Type.String({
|
|
17
|
+
description: "Search terms (title, abstract, author, etc.)",
|
|
18
|
+
}),
|
|
19
|
+
max_results: Type.Optional(
|
|
20
|
+
Type.Integer({
|
|
21
|
+
minimum: 1,
|
|
22
|
+
maximum: 50,
|
|
23
|
+
default: 5,
|
|
24
|
+
description: "Maximum results to return (1-50, default 5)",
|
|
25
|
+
})
|
|
26
|
+
),
|
|
27
|
+
categories: Type.Optional(
|
|
28
|
+
Type.Array(Type.String(), {
|
|
29
|
+
default: [],
|
|
30
|
+
description: "arXiv categories to filter (e.g., cs.AI, cs.LG, cs.CL)",
|
|
31
|
+
})
|
|
32
|
+
),
|
|
33
|
+
sort_by: Type.Optional(
|
|
34
|
+
Type.Enum({ relevance: "relevance", lastUpdatedDate: "lastUpdatedDate" }, {
|
|
35
|
+
default: "relevance",
|
|
36
|
+
description: "Sort order: relevance or lastUpdatedDate",
|
|
37
|
+
})
|
|
38
|
+
),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const ARXIV_API = "http://export.arxiv.org/api/query"
|
|
42
|
+
const TIMEOUT_MS = 15_000
|
|
43
|
+
const ABSTRACT_MAX = 300
|
|
44
|
+
|
|
45
|
+
// Extract text between XML tags (simple, no dependency needed).
|
|
46
|
+
function extractTag(xml: string, tag: string): string | null {
|
|
47
|
+
const re = new RegExp(`<${tag}[^>]*>([^<]*)</${tag}>`, "i")
|
|
48
|
+
const match = xml.match(re)
|
|
49
|
+
return match ? match[1].replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"') : null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Extract all tags of a given name.
|
|
53
|
+
function extractAllTags(xml: string, tag: string): string[] {
|
|
54
|
+
const re = new RegExp(`<${tag}[^>]*>([^<]*)</${tag}>`, "gi")
|
|
55
|
+
const results: string[] = []
|
|
56
|
+
let m
|
|
57
|
+
while ((m = re.exec(xml)) !== null) {
|
|
58
|
+
results.push(m[1].replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"'))
|
|
59
|
+
}
|
|
60
|
+
return results
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Extract text between <entry> tags.
|
|
64
|
+
function extractEntries(xml: string): string[] {
|
|
65
|
+
const re = /<entry>([\s\S]*?)<\/entry>/gi
|
|
66
|
+
const results: string[] = []
|
|
67
|
+
let m
|
|
68
|
+
while ((m = re.exec(xml)) !== null) {
|
|
69
|
+
results.push(m[1])
|
|
70
|
+
}
|
|
71
|
+
return results
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function searchArxiv(query: string, maxResults: number, categories: string[], sortBy: string): Promise<AgentToolResult<undefined>> {
|
|
75
|
+
// Build search query with category filters.
|
|
76
|
+
const searchTerms: string[] = []
|
|
77
|
+
if (query) {
|
|
78
|
+
searchTerms.push(query)
|
|
79
|
+
}
|
|
80
|
+
if (categories.length > 0) {
|
|
81
|
+
const catQuery = categories.map((c) => `cat:${c}`).join(" OR ")
|
|
82
|
+
searchTerms.push(`(${catQuery})`)
|
|
83
|
+
}
|
|
84
|
+
const searchQuery = searchTerms.join(" AND ")
|
|
85
|
+
|
|
86
|
+
// Build URL.
|
|
87
|
+
const params = new URLSearchParams({
|
|
88
|
+
search_query: searchQuery,
|
|
89
|
+
max_results: String(Math.min(maxResults, 50)),
|
|
90
|
+
sort_by: sortBy,
|
|
91
|
+
sort_order: "descending",
|
|
92
|
+
})
|
|
93
|
+
const url = `${ARXIV_API}?${params}`
|
|
94
|
+
|
|
95
|
+
const controller = new AbortController()
|
|
96
|
+
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS)
|
|
97
|
+
try {
|
|
98
|
+
const response = await fetch(url, { signal: controller.signal })
|
|
99
|
+
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
throw new Error(`arXiv API returned ${response.status} ${response.statusText}`)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const xml = await response.text()
|
|
105
|
+
const entries = extractEntries(xml)
|
|
106
|
+
|
|
107
|
+
if (entries.length === 0) {
|
|
108
|
+
return {
|
|
109
|
+
content: [{ type: "text", text: `No results found for: "${query}"` }],
|
|
110
|
+
details: undefined,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Parse each entry.
|
|
115
|
+
const results = entries.map((entryXml, i) => {
|
|
116
|
+
const title = extractTag(entryXml, "title") ?? "No title"
|
|
117
|
+
const abstract = (extractTag(entryXml, "summary") ?? "").replace(/\n/g, " ").trim()
|
|
118
|
+
const authors = extractAllTags(entryXml, "name")
|
|
119
|
+
const categories = extractAllTags(entryXml, "term")
|
|
120
|
+
const id = extractTag(entryXml, "id") ?? ""
|
|
121
|
+
const published = extractTag(entryXml, "published") ?? ""
|
|
122
|
+
|
|
123
|
+
// Extract PDF URL from <link> tags.
|
|
124
|
+
let pdfUrl = ""
|
|
125
|
+
const linkRe = /<link[^>]*title="pdf"[^>]*href="([^"]*)"/i
|
|
126
|
+
const pdfMatch = entryXml.match(linkRe)
|
|
127
|
+
if (pdfMatch) pdfUrl = pdfMatch[1]
|
|
128
|
+
else {
|
|
129
|
+
// Fallback: construct from arXiv ID.
|
|
130
|
+
const idRe = /arxiv\.org\/abs\/(.+)$/
|
|
131
|
+
const idMatch = id.match(idRe)
|
|
132
|
+
if (idMatch) pdfUrl = `https://arxiv.org/pdf/${idMatch[1]}`
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Truncate abstract.
|
|
136
|
+
const truncatedAbstract = abstract.length > ABSTRACT_MAX
|
|
137
|
+
? abstract.slice(0, ABSTRACT_MAX) + "..."
|
|
138
|
+
: abstract
|
|
139
|
+
|
|
140
|
+
return [
|
|
141
|
+
`${i + 1}. ${title}`,
|
|
142
|
+
` Authors: ${authors.join(", ")}`,
|
|
143
|
+
` Published: ${published}`,
|
|
144
|
+
` Categories: ${categories.join(", ")}`,
|
|
145
|
+
` Abstract: ${truncatedAbstract}`,
|
|
146
|
+
` PDF: ${pdfUrl}`,
|
|
147
|
+
].join("\n")
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const header = `arXiv search results for "${query}" (${results.length} papers found)`
|
|
151
|
+
return {
|
|
152
|
+
content: [{ type: "text", text: header + "\n\n" + results.join("\n\n") }],
|
|
153
|
+
details: undefined,
|
|
154
|
+
}
|
|
155
|
+
} catch (err) {
|
|
156
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
157
|
+
return {
|
|
158
|
+
content: [{
|
|
159
|
+
type: "text",
|
|
160
|
+
text: `arxiv_search error: ${msg}. Ensure the arXiv API is reachable at ${ARXIV_API}.`,
|
|
161
|
+
}],
|
|
162
|
+
details: undefined,
|
|
163
|
+
}
|
|
164
|
+
} finally {
|
|
165
|
+
clearTimeout(timeout)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function createArxivSearchToolDefinition(): ToolDefinition<typeof arxivSearchSchema, undefined> {
|
|
170
|
+
return {
|
|
171
|
+
name: "arxiv_search",
|
|
172
|
+
label: "arXiv Search",
|
|
173
|
+
description:
|
|
174
|
+
"Search academic papers on arXiv (2.3M+ papers). " +
|
|
175
|
+
"Free, no API key. Returns titles, authors, abstracts, categories, and PDF links. " +
|
|
176
|
+
"Abstracts are truncated to ~300 chars.",
|
|
177
|
+
parameters: arxivSearchSchema,
|
|
178
|
+
execute: async (_toolCallId, params) => {
|
|
179
|
+
const query = params.query
|
|
180
|
+
const maxResults = params.max_results ?? 5
|
|
181
|
+
const categories = params.categories ?? []
|
|
182
|
+
const sortBy = params.sort_by ?? "relevance"
|
|
183
|
+
return searchArxiv(query, maxResults, categories, sortBy)
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// check_room — read a sub-room's goal status and recent transcript so the
|
|
2
|
+
// spawning agent can decide whether to keep waiting or synthesize the result.
|
|
3
|
+
|
|
4
|
+
import { Type } from "typebox"
|
|
5
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent"
|
|
6
|
+
import type { RoomOrchestrator } from "../orchestrator.js"
|
|
7
|
+
|
|
8
|
+
const checkRoomSchema = Type.Object({
|
|
9
|
+
roomId: Type.String({
|
|
10
|
+
description: "The roomId returned by spawn_room.",
|
|
11
|
+
}),
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export function createCheckRoomToolDefinition(
|
|
15
|
+
orchestrator: RoomOrchestrator,
|
|
16
|
+
): ToolDefinition<typeof checkRoomSchema, undefined> {
|
|
17
|
+
return {
|
|
18
|
+
name: "check_room",
|
|
19
|
+
label: "Check Sub-Room",
|
|
20
|
+
description:
|
|
21
|
+
"Read a sub-room's current goal status (idle/running/completed/failed) and its last few " +
|
|
22
|
+
"transcript messages. Poll this after spawn_room until the goal is completed or failed, " +
|
|
23
|
+
"then synthesize the result and destroy the room.",
|
|
24
|
+
parameters: checkRoomSchema,
|
|
25
|
+
execute: async (_toolCallId, params) => {
|
|
26
|
+
const r = orchestrator.checkRoom(params.roomId)
|
|
27
|
+
if (!r.found) {
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: "text", text: `check_room: no room with id "${params.roomId}".` }],
|
|
30
|
+
details: undefined,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const header =
|
|
34
|
+
`Room "${r.name}" (${r.roomId}) — goal status: ${r.goalStatus}` +
|
|
35
|
+
(r.goalText ? `\nGoal: ${r.goalText}` : "")
|
|
36
|
+
const body = r.lastMessages && r.lastMessages.length > 0
|
|
37
|
+
? `\n\nLast messages:\n${r.lastMessages.join("\n")}`
|
|
38
|
+
: "\n\n(no transcript yet)"
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: "text", text: header + body }],
|
|
41
|
+
details: undefined,
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// destroy_room — tear down a sub-room after its goal is done. Unmounts any
|
|
2
|
+
// sshfs target the room was scoped to. The default room cannot be destroyed.
|
|
3
|
+
|
|
4
|
+
import { Type } from "typebox"
|
|
5
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent"
|
|
6
|
+
import type { RoomOrchestrator } from "../orchestrator.js"
|
|
7
|
+
|
|
8
|
+
const destroyRoomSchema = Type.Object({
|
|
9
|
+
roomId: Type.String({
|
|
10
|
+
description: "The roomId of the sub-room to destroy.",
|
|
11
|
+
}),
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export function createDestroyRoomToolDefinition(
|
|
15
|
+
orchestrator: RoomOrchestrator,
|
|
16
|
+
): ToolDefinition<typeof destroyRoomSchema, undefined> {
|
|
17
|
+
return {
|
|
18
|
+
name: "destroy_room",
|
|
19
|
+
label: "Destroy Sub-Room",
|
|
20
|
+
description:
|
|
21
|
+
"Destroy a sub-room once you have collected its result. Unmounts any sshfs target. " +
|
|
22
|
+
"Call this after check_room shows the goal is completed or failed, to free resources.",
|
|
23
|
+
parameters: destroyRoomSchema,
|
|
24
|
+
execute: async (_toolCallId, params) => {
|
|
25
|
+
try {
|
|
26
|
+
const ok = await orchestrator.destroyRoom(params.roomId)
|
|
27
|
+
return {
|
|
28
|
+
content: [{
|
|
29
|
+
type: "text",
|
|
30
|
+
text: ok
|
|
31
|
+
? `Destroyed room "${params.roomId}".`
|
|
32
|
+
: `destroy_room: no room with id "${params.roomId}" (or it cannot be destroyed).`,
|
|
33
|
+
}],
|
|
34
|
+
details: undefined,
|
|
35
|
+
}
|
|
36
|
+
} catch (err) {
|
|
37
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
38
|
+
return {
|
|
39
|
+
content: [{ type: "text", text: `destroy_room error: ${msg}` }],
|
|
40
|
+
details: undefined,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Custom tools registry — extension point for Pipeline-MoE.
|
|
2
|
+
// Each custom tool is a standalone ToolDefinition. Add new tools here
|
|
3
|
+
// and register them in the index. The allowlist in `persona.tools` gates
|
|
4
|
+
// which tools each agent gets.
|
|
5
|
+
|
|
6
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent"
|
|
7
|
+
import { createWebSearchToolDefinition } from "./web-search.js"
|
|
8
|
+
import { createWebReadToolDefinition } from "./web-read.js"
|
|
9
|
+
import { createYoutubeTranscriptToolDefinition } from "./youtube-transcript.js"
|
|
10
|
+
import { createArxivSearchToolDefinition } from "./arxiv-search.js"
|
|
11
|
+
import { createYoucomSearchToolDefinition } from "./youcom-search.js"
|
|
12
|
+
import { createSpawnRoomToolDefinition } from "./spawn-room.js"
|
|
13
|
+
import { createCheckRoomToolDefinition } from "./check-room.js"
|
|
14
|
+
import { createStopRoomToolDefinition } from "./stop-room.js"
|
|
15
|
+
import { createDestroyRoomToolDefinition } from "./destroy-room.js"
|
|
16
|
+
import type { RoomOrchestrator } from "../orchestrator.js"
|
|
17
|
+
|
|
18
|
+
/** Runtime context the tool registry needs to build context-dependent tools.
|
|
19
|
+
* Orchestration tools (spawn/check/destroy room) require a live orchestrator;
|
|
20
|
+
* they are only built when one is supplied. */
|
|
21
|
+
export interface ToolContext {
|
|
22
|
+
orchestrator?: RoomOrchestrator
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Orchestration tool names — gated on a RoomOrchestrator being present, not on
|
|
26
|
+
* the static TOOLS registry. Only orchestrator personas (the planner) get them. */
|
|
27
|
+
export const ORCHESTRATION_TOOLS = ["spawn_room", "check_room", "stop_room", "destroy_room"] as const
|
|
28
|
+
|
|
29
|
+
// Registry of tool name → factory function.
|
|
30
|
+
// Add new tools here — each tool is a self-contained module.
|
|
31
|
+
// Factory is typed loosely to avoid contravariance issues with ToolDefinition
|
|
32
|
+
// generics — the actual type safety is enforced in each tool's definition.
|
|
33
|
+
const TOOLS: Array<{ name: string; factory: () => unknown }> = [
|
|
34
|
+
{ name: "web_search", factory: createWebSearchToolDefinition },
|
|
35
|
+
{ name: "web_read", factory: createWebReadToolDefinition },
|
|
36
|
+
{ name: "youtube_transcript", factory: createYoutubeTranscriptToolDefinition },
|
|
37
|
+
{ name: "arxiv_search", factory: createArxivSearchToolDefinition },
|
|
38
|
+
{ name: "youcom_search", factory: createYoucomSearchToolDefinition },
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
/** Build custom tool definitions for the given tool name allowlist.
|
|
42
|
+
* Only returns tools whose name appears in the allowlist. Orchestration tools
|
|
43
|
+
* (spawn/check/destroy room) are only built when `ctx.orchestrator` is supplied. */
|
|
44
|
+
export function buildCustomTools(toolNames: string[], ctx?: ToolContext): ToolDefinition[] {
|
|
45
|
+
const wanted = new Set(toolNames)
|
|
46
|
+
const tools: ToolDefinition[] = []
|
|
47
|
+
|
|
48
|
+
for (const { name, factory } of TOOLS) {
|
|
49
|
+
if (wanted.has(name)) {
|
|
50
|
+
tools.push(factory() as ToolDefinition)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Orchestration tools — context-gated. Require a live orchestrator reference
|
|
55
|
+
// captured at execution time. Absent orchestrator → these names are silently
|
|
56
|
+
// ignored (same contract as unknown tool names).
|
|
57
|
+
const wantsOrch = [...ORCHESTRATION_TOOLS].filter(n => wanted.has(n))
|
|
58
|
+
if (wantsOrch.length > 0) {
|
|
59
|
+
console.log(`[buildCustomTools] orchestration tools requested: ${wantsOrch.join(", ")}; ctx?.orchestrator = ${!!ctx?.orchestrator}`)
|
|
60
|
+
}
|
|
61
|
+
if (ctx?.orchestrator) {
|
|
62
|
+
const orch = ctx.orchestrator
|
|
63
|
+
if (wanted.has("spawn_room")) tools.push(createSpawnRoomToolDefinition(orch) as ToolDefinition)
|
|
64
|
+
if (wanted.has("check_room")) tools.push(createCheckRoomToolDefinition(orch) as ToolDefinition)
|
|
65
|
+
if (wanted.has("stop_room")) tools.push(createStopRoomToolDefinition(orch) as ToolDefinition)
|
|
66
|
+
if (wanted.has("destroy_room")) tools.push(createDestroyRoomToolDefinition(orch) as ToolDefinition)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return tools
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Get all available custom tool names (for validation / UI). */
|
|
73
|
+
export function availableCustomTools(): string[] {
|
|
74
|
+
return TOOLS.map((t) => t.name)
|
|
75
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// spawn_room — create a parallel sub-room with its own agents and a goal.
|
|
2
|
+
// The room runs independently in the background; this tool returns immediately
|
|
3
|
+
// with the roomId. Poll progress with check_room, clean up with destroy_room.
|
|
4
|
+
|
|
5
|
+
import { Type } from "typebox"
|
|
6
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent"
|
|
7
|
+
import type { RoomOrchestrator } from "../orchestrator.js"
|
|
8
|
+
|
|
9
|
+
const spawnRoomSchema = Type.Object({
|
|
10
|
+
name: Type.String({
|
|
11
|
+
description: "Display name for the sub-room (e.g. 'audit-auth-flow').",
|
|
12
|
+
}),
|
|
13
|
+
goal: Type.String({
|
|
14
|
+
description:
|
|
15
|
+
"The goal the sub-room's agents will work on autonomously. Be specific and self-contained — the sub-room does not share your conversation context.",
|
|
16
|
+
}),
|
|
17
|
+
preset: Type.Optional(
|
|
18
|
+
Type.String({
|
|
19
|
+
description:
|
|
20
|
+
"Preset roster name (from presets/, e.g. 'local-default'). Omit to use the default roster.",
|
|
21
|
+
}),
|
|
22
|
+
),
|
|
23
|
+
workspaceDir: Type.Optional(
|
|
24
|
+
Type.String({
|
|
25
|
+
description:
|
|
26
|
+
"Working directory scope — a local path or an sshfs target (user@host:/path). Omit for the pipeline workspace.",
|
|
27
|
+
}),
|
|
28
|
+
),
|
|
29
|
+
goalMode: Type.Optional(
|
|
30
|
+
Type.Union([Type.Literal("auto"), Type.Literal("eval")], {
|
|
31
|
+
description:
|
|
32
|
+
"Goal completion mode. 'auto' (default): the goal completes when the sub-room's pipeline " +
|
|
33
|
+
"drains. 'eval': after each pass the evaluator agent re-enters the sub-room, verifies " +
|
|
34
|
+
"the goal independently with tools, and either dispatches more work or declares GOAL_MET. Use " +
|
|
35
|
+
"'eval' for iterative goals that need a verification loop.",
|
|
36
|
+
}),
|
|
37
|
+
),
|
|
38
|
+
goalEvaluator: Type.Optional(
|
|
39
|
+
Type.String({
|
|
40
|
+
description:
|
|
41
|
+
"Agent id that evaluates the goal in 'eval' mode (e.g. 'planner', 'auditor'). Defaults to " +
|
|
42
|
+
"'planner'. Must match a persona id in the sub-room's roster, or the goal will auto-complete " +
|
|
43
|
+
"without verification.",
|
|
44
|
+
}),
|
|
45
|
+
),
|
|
46
|
+
maxGoalIterations: Type.Optional(
|
|
47
|
+
Type.Integer({
|
|
48
|
+
minimum: 1,
|
|
49
|
+
maximum: 50,
|
|
50
|
+
description:
|
|
51
|
+
"Max evaluation iterations before the goal auto-fails in 'eval' mode. Default 10. Each " +
|
|
52
|
+
"iteration is one evaluator verification pass plus any agents it dispatches.",
|
|
53
|
+
}),
|
|
54
|
+
),
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
export function createSpawnRoomToolDefinition(
|
|
58
|
+
orchestrator: RoomOrchestrator,
|
|
59
|
+
): ToolDefinition<typeof spawnRoomSchema, undefined> {
|
|
60
|
+
return {
|
|
61
|
+
name: "spawn_room",
|
|
62
|
+
label: "Spawn Sub-Room",
|
|
63
|
+
description:
|
|
64
|
+
"Create a new parallel room with its own agents and give it a goal. The room runs " +
|
|
65
|
+
"independently in the background — use this to delegate a bounded, self-contained workstream. " +
|
|
66
|
+
"Returns the roomId; poll progress with check_room and clean up with destroy_room. " +
|
|
67
|
+
"The sub-room does NOT share your conversation, so the goal must be fully self-contained.",
|
|
68
|
+
parameters: spawnRoomSchema,
|
|
69
|
+
execute: async (_toolCallId, params) => {
|
|
70
|
+
try {
|
|
71
|
+
const r = await orchestrator.spawnRoom({
|
|
72
|
+
name: params.name,
|
|
73
|
+
goal: params.goal,
|
|
74
|
+
preset: params.preset,
|
|
75
|
+
workspaceDir: params.workspaceDir,
|
|
76
|
+
goalMode: params.goalMode,
|
|
77
|
+
goalEvaluator: params.goalEvaluator,
|
|
78
|
+
maxGoalIterations: params.maxGoalIterations,
|
|
79
|
+
})
|
|
80
|
+
return {
|
|
81
|
+
content: [{
|
|
82
|
+
type: "text",
|
|
83
|
+
text:
|
|
84
|
+
`Spawned room "${r.name}" — roomId: ${r.roomId}, status: ${r.goalStatus}.\n` +
|
|
85
|
+
`Poll with check_room({ roomId: "${r.roomId}" }) until status is "completed" or "failed", ` +
|
|
86
|
+
`then destroy_room({ roomId: "${r.roomId}" }) to clean up.`,
|
|
87
|
+
}],
|
|
88
|
+
details: undefined,
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
92
|
+
return {
|
|
93
|
+
content: [{ type: "text", text: `spawn_room error: ${msg}` }],
|
|
94
|
+
details: undefined,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// stop_room — halt a sub-room's in-flight work WITHOUT destroying it. Aborts
|
|
2
|
+
// running agents and cancels the goal (status → "cancelled"); the room and its
|
|
3
|
+
// transcript survive so you can inspect what went wrong, then decide whether to
|
|
4
|
+
// re-dispatch or destroy_room to free resources. The default room is protected.
|
|
5
|
+
|
|
6
|
+
import { Type } from "typebox"
|
|
7
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent"
|
|
8
|
+
import type { RoomOrchestrator } from "../orchestrator.js"
|
|
9
|
+
|
|
10
|
+
const stopRoomSchema = Type.Object({
|
|
11
|
+
roomId: Type.String({
|
|
12
|
+
description: "The roomId of the sub-room to stop.",
|
|
13
|
+
}),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export function createStopRoomToolDefinition(
|
|
17
|
+
orchestrator: RoomOrchestrator,
|
|
18
|
+
): ToolDefinition<typeof stopRoomSchema, undefined> {
|
|
19
|
+
return {
|
|
20
|
+
name: "stop_room",
|
|
21
|
+
label: "Stop Sub-Room",
|
|
22
|
+
description:
|
|
23
|
+
"Halt a sub-room that is running away or no longer needed, WITHOUT destroying it. Aborts its " +
|
|
24
|
+
"running agents and cancels its goal (status becomes 'cancelled'); the room and its transcript " +
|
|
25
|
+
"remain intact so you can inspect what happened with check_room. Call destroy_room afterwards " +
|
|
26
|
+
"to free resources. Cannot stop the default room.",
|
|
27
|
+
parameters: stopRoomSchema,
|
|
28
|
+
execute: async (_toolCallId, params) => {
|
|
29
|
+
try {
|
|
30
|
+
const ok = await orchestrator.stopRoom(params.roomId)
|
|
31
|
+
return {
|
|
32
|
+
content: [{
|
|
33
|
+
type: "text",
|
|
34
|
+
text: ok
|
|
35
|
+
? `Stopped room "${params.roomId}" — goal cancelled, agents aborted. The room and its ` +
|
|
36
|
+
`transcript are intact: check_room to inspect, or destroy_room to free resources.`
|
|
37
|
+
: `stop_room: no room with id "${params.roomId}" (or it cannot be stopped — the default room is protected).`,
|
|
38
|
+
}],
|
|
39
|
+
details: undefined,
|
|
40
|
+
}
|
|
41
|
+
} catch (err) {
|
|
42
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: "text", text: `stop_room error: ${msg}` }],
|
|
45
|
+
details: undefined,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// web_read — Jina Reader tool definition
|
|
2
|
+
// Extracts clean markdown content from any URL via Jina Reader.
|
|
3
|
+
// Free, no API key, 100 req/min. The natural follow-up to web_search:
|
|
4
|
+
// search finds URLs, web_read extracts their content.
|
|
5
|
+
|
|
6
|
+
import { Type } from "typebox"
|
|
7
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent"
|
|
8
|
+
import type { AgentToolResult } from "@earendil-works/pi-coding-agent"
|
|
9
|
+
|
|
10
|
+
// Minimal text content type (mirrors pi-ai TextContent — not re-exported).
|
|
11
|
+
interface TextContent {
|
|
12
|
+
type: "text"
|
|
13
|
+
text: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const JinaResponse = Type.Object({
|
|
17
|
+
code: Type.Number(),
|
|
18
|
+
status: Type.Number(),
|
|
19
|
+
data: Type.Optional(Type.Object({
|
|
20
|
+
title: Type.String(),
|
|
21
|
+
content: Type.String(),
|
|
22
|
+
url: Type.String(),
|
|
23
|
+
})),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const webReadSchema = Type.Object({
|
|
27
|
+
url: Type.String({
|
|
28
|
+
description: "The URL to extract content from",
|
|
29
|
+
}),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const JINA_URL = "https://r.jina.ai"
|
|
33
|
+
const TIMEOUT_MS = 15_000
|
|
34
|
+
const MAX_CONTENT_LENGTH = 16000
|
|
35
|
+
|
|
36
|
+
async function readUrl(url: string): Promise<AgentToolResult<undefined>> {
|
|
37
|
+
const jinaUrl = `${JINA_URL}/${url}`
|
|
38
|
+
|
|
39
|
+
const controller = new AbortController()
|
|
40
|
+
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS)
|
|
41
|
+
try {
|
|
42
|
+
const response = await fetch(jinaUrl, {
|
|
43
|
+
signal: controller.signal,
|
|
44
|
+
headers: { "Accept": "application/json" },
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw new Error(`Jina Reader returned ${response.status} ${response.statusText}`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const body = await response.json()
|
|
52
|
+
const data = (body as { data?: { title: string; content: string; url: string } }).data
|
|
53
|
+
|
|
54
|
+
if (!data) {
|
|
55
|
+
throw new Error("No content returned by Jina Reader")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Truncate to avoid context explosion.
|
|
59
|
+
const content = data.content.length > MAX_CONTENT_LENGTH
|
|
60
|
+
? data.content.slice(0, MAX_CONTENT_LENGTH) + "\n\n[content truncated — " + data.content.length + " chars total]"
|
|
61
|
+
: data.content
|
|
62
|
+
|
|
63
|
+
const lines = [
|
|
64
|
+
`# ${data.title}`,
|
|
65
|
+
`Source: ${data.url}`,
|
|
66
|
+
"",
|
|
67
|
+
content,
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
72
|
+
details: undefined,
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
76
|
+
return {
|
|
77
|
+
content: [{
|
|
78
|
+
type: "text",
|
|
79
|
+
text: `web_read error: ${msg}. Ensure the URL is accessible and Jina Reader is reachable at ${JINA_URL}.`,
|
|
80
|
+
}],
|
|
81
|
+
details: undefined,
|
|
82
|
+
}
|
|
83
|
+
} finally {
|
|
84
|
+
clearTimeout(timeout)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function createWebReadToolDefinition(): ToolDefinition<typeof webReadSchema, undefined> {
|
|
89
|
+
return {
|
|
90
|
+
name: "web_read",
|
|
91
|
+
label: "Web Read",
|
|
92
|
+
description:
|
|
93
|
+
"Extract clean markdown content from any web page via Jina Reader. " +
|
|
94
|
+
"Free, no API key, 100 req/min. The natural follow-up to web_search: " +
|
|
95
|
+
"search finds URLs, web_read extracts their content. " +
|
|
96
|
+
"Content is truncated to ~16000 chars to avoid context explosion — " +
|
|
97
|
+
"ask for a specific section if you need more detail.",
|
|
98
|
+
parameters: webReadSchema,
|
|
99
|
+
execute: async (_toolCallId, params) => {
|
|
100
|
+
const url = params.url
|
|
101
|
+
return readUrl(url)
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
}
|