@dungle-scrubs/tallow 0.8.24 → 0.8.26
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.
- package/dist/auth-hardening.d.ts +12 -0
- package/dist/auth-hardening.d.ts.map +1 -1
- package/dist/auth-hardening.js +30 -7
- package/dist/auth-hardening.js.map +1 -1
- package/dist/cli.js +5 -0
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/install.js +2 -2
- package/dist/install.js.map +1 -1
- package/dist/interactive-mode-patch.d.ts.map +1 -1
- package/dist/interactive-mode-patch.js +119 -7
- package/dist/interactive-mode-patch.js.map +1 -1
- package/dist/model-metadata-overrides.d.ts +19 -0
- package/dist/model-metadata-overrides.d.ts.map +1 -0
- package/dist/model-metadata-overrides.js +38 -0
- package/dist/model-metadata-overrides.js.map +1 -0
- package/dist/sdk.d.ts +2 -0
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +28 -1
- package/dist/sdk.js.map +1 -1
- package/extensions/__integration__/teams-runtime.test.ts +22 -1
- package/extensions/_shared/__tests__/shell-policy.test.ts +197 -0
- package/extensions/_shared/shell-policy.ts +27 -0
- package/extensions/background-task-tool/index.ts +2 -1
- package/extensions/bash-tool-enhanced/index.ts +2 -1
- package/extensions/custom-footer/__tests__/index.test.ts +29 -0
- package/extensions/custom-footer/context-display.ts +49 -0
- package/extensions/custom-footer/index.ts +10 -23
- package/extensions/permissions/index.ts +31 -10
- package/extensions/plan-mode-tool/__tests__/index.test.ts +32 -2
- package/extensions/plan-mode-tool/index.ts +6 -1
- package/extensions/skill-commands/__tests__/shared-skills-dirs.test.ts +113 -0
- package/extensions/skill-commands/index.ts +62 -5
- package/extensions/slash-command-bridge/index.ts +30 -1
- package/extensions/subagent-tool/__tests__/process-liveness.test.ts +42 -3
- package/extensions/subagent-tool/process.ts +132 -21
- package/extensions/tasks/__tests__/store.test.ts +26 -2
- package/extensions/tasks/commands/register-tasks-extension.ts +2 -2
- package/extensions/tasks/index.ts +5 -5
- package/extensions/tasks/state/index.ts +90 -36
- package/extensions/teams-tool/__tests__/archive-store.test.ts +98 -0
- package/extensions/teams-tool/__tests__/peer-messaging.test.ts +26 -0
- package/extensions/teams-tool/archive-store.ts +200 -0
- package/extensions/teams-tool/sessions/spawn.ts +244 -71
- package/extensions/teams-tool/tools/register-extension.ts +146 -105
- package/extensions/teams-tool/tools/teammate-tools.ts +43 -1
- package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keys.js +59 -7
- package/node_modules/@mariozechner/pi-tui/dist/keys.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/package.json +1 -1
- package/node_modules/@mariozechner/pi-tui/src/keys.ts +71 -7
- package/package.json +5 -5
- package/skills/tallow-expert/SKILL.md +1 -1
- package/templates/agents/architect.md +13 -5
- package/templates/agents/debug.md +3 -3
- package/templates/agents/explore.md +9 -2
- package/templates/agents/refactor.md +2 -2
- package/templates/agents/scout.md +3 -2
- package/extensions/__integration__/plan-rejection-feedback.test.ts +0 -272
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent archive storage for teams-tool.
|
|
3
|
+
*
|
|
4
|
+
* Archives are stored on disk so `team_resume` survives process restarts and
|
|
5
|
+
* session shutdown. Runtime state still uses the in-memory store; this module
|
|
6
|
+
* only handles serialization and persistence of archived snapshots.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { atomicWriteFileSync } from "../_shared/atomic-write.js";
|
|
12
|
+
import { getTallowPath } from "../_shared/tallow-paths.js";
|
|
13
|
+
import type { ArchivedTeam, TeamMessage } from "./store.js";
|
|
14
|
+
|
|
15
|
+
interface SerializedTeamMessage {
|
|
16
|
+
readonly content: string;
|
|
17
|
+
readonly from: string;
|
|
18
|
+
readonly readBy: readonly string[];
|
|
19
|
+
readonly timestamp: number;
|
|
20
|
+
readonly to: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface SerializedArchivedTeam {
|
|
24
|
+
readonly archivedAt: number;
|
|
25
|
+
readonly messages: readonly SerializedTeamMessage[];
|
|
26
|
+
readonly name: string;
|
|
27
|
+
readonly taskCounter: number;
|
|
28
|
+
readonly tasks: ArchivedTeam["tasks"];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the directory that stores archived teams.
|
|
33
|
+
*
|
|
34
|
+
* @returns Absolute archive directory path under the active tallow home
|
|
35
|
+
*/
|
|
36
|
+
export function getTeamArchivesDir(): string {
|
|
37
|
+
return getTallowPath("team-archives");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Ensure the archive directory exists before reading or writing.
|
|
42
|
+
*
|
|
43
|
+
* @returns Archive directory path
|
|
44
|
+
*/
|
|
45
|
+
function ensureTeamArchivesDir(): string {
|
|
46
|
+
const dir = getTeamArchivesDir();
|
|
47
|
+
mkdirSync(dir, { recursive: true });
|
|
48
|
+
return dir;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the archive file path for one team name.
|
|
53
|
+
*
|
|
54
|
+
* @param teamName - Team name to encode into a stable file name
|
|
55
|
+
* @returns Absolute JSON file path
|
|
56
|
+
*/
|
|
57
|
+
function getArchiveFilePath(teamName: string): string {
|
|
58
|
+
return join(ensureTeamArchivesDir(), `${encodeURIComponent(teamName)}.json`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Convert a runtime TeamMessage into a JSON-safe form.
|
|
63
|
+
*
|
|
64
|
+
* @param message - Runtime message with Set-based read tracking
|
|
65
|
+
* @returns Serializable message record
|
|
66
|
+
*/
|
|
67
|
+
function serializeTeamMessage(message: TeamMessage): SerializedTeamMessage {
|
|
68
|
+
return {
|
|
69
|
+
content: message.content,
|
|
70
|
+
from: message.from,
|
|
71
|
+
readBy: [...message.readBy].sort(),
|
|
72
|
+
timestamp: message.timestamp,
|
|
73
|
+
to: message.to,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Convert an archived team into a JSON-safe form.
|
|
79
|
+
*
|
|
80
|
+
* @param archived - Archived team snapshot from the runtime store
|
|
81
|
+
* @returns Serializable archive payload
|
|
82
|
+
*/
|
|
83
|
+
function serializeArchivedTeam(archived: ArchivedTeam): SerializedArchivedTeam {
|
|
84
|
+
return {
|
|
85
|
+
archivedAt: archived.archivedAt,
|
|
86
|
+
messages: archived.messages.map(serializeTeamMessage),
|
|
87
|
+
name: archived.name,
|
|
88
|
+
taskCounter: archived.taskCounter,
|
|
89
|
+
tasks: archived.tasks,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Convert a serialized message back into the runtime representation.
|
|
95
|
+
*
|
|
96
|
+
* @param message - JSON-parsed message payload
|
|
97
|
+
* @returns Runtime message with Set-based read tracking
|
|
98
|
+
*/
|
|
99
|
+
function deserializeTeamMessage(message: SerializedTeamMessage): TeamMessage {
|
|
100
|
+
return {
|
|
101
|
+
content: message.content,
|
|
102
|
+
from: message.from,
|
|
103
|
+
readBy: new Set(message.readBy),
|
|
104
|
+
timestamp: message.timestamp,
|
|
105
|
+
to: message.to,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Parse an archived-team JSON payload.
|
|
111
|
+
*
|
|
112
|
+
* @param raw - Raw JSON string from disk
|
|
113
|
+
* @returns Parsed archive, or undefined when malformed
|
|
114
|
+
*/
|
|
115
|
+
function deserializeArchivedTeam(raw: string): ArchivedTeam | undefined {
|
|
116
|
+
try {
|
|
117
|
+
const parsed = JSON.parse(raw) as Partial<SerializedArchivedTeam>;
|
|
118
|
+
if (
|
|
119
|
+
typeof parsed.name !== "string" ||
|
|
120
|
+
typeof parsed.archivedAt !== "number" ||
|
|
121
|
+
typeof parsed.taskCounter !== "number" ||
|
|
122
|
+
!Array.isArray(parsed.tasks) ||
|
|
123
|
+
!Array.isArray(parsed.messages)
|
|
124
|
+
) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
archivedAt: parsed.archivedAt,
|
|
129
|
+
messages: parsed.messages.map((message) => deserializeTeamMessage(message)),
|
|
130
|
+
name: parsed.name,
|
|
131
|
+
taskCounter: parsed.taskCounter,
|
|
132
|
+
tasks: parsed.tasks,
|
|
133
|
+
};
|
|
134
|
+
} catch {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Persist one archived team snapshot to disk.
|
|
141
|
+
*
|
|
142
|
+
* @param archived - Archived team snapshot to write
|
|
143
|
+
* @returns Nothing
|
|
144
|
+
*/
|
|
145
|
+
export function writeArchivedTeamToDisk(archived: ArchivedTeam): void {
|
|
146
|
+
const filePath = getArchiveFilePath(archived.name);
|
|
147
|
+
atomicWriteFileSync(filePath, JSON.stringify(serializeArchivedTeam(archived), null, 2));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Delete one archived team snapshot from disk.
|
|
152
|
+
*
|
|
153
|
+
* @param teamName - Team whose archive should be removed
|
|
154
|
+
* @returns Nothing
|
|
155
|
+
*/
|
|
156
|
+
export function deleteArchivedTeamFromDisk(teamName: string): void {
|
|
157
|
+
const filePath = getArchiveFilePath(teamName);
|
|
158
|
+
if (!existsSync(filePath)) return;
|
|
159
|
+
unlinkSync(filePath);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Load one archived team snapshot from disk.
|
|
164
|
+
*
|
|
165
|
+
* @param teamName - Team whose archive should be read
|
|
166
|
+
* @returns Archived snapshot, or undefined when missing or malformed
|
|
167
|
+
*/
|
|
168
|
+
export function loadArchivedTeamFromDisk(teamName: string): ArchivedTeam | undefined {
|
|
169
|
+
const filePath = getArchiveFilePath(teamName);
|
|
170
|
+
if (!existsSync(filePath)) return undefined;
|
|
171
|
+
try {
|
|
172
|
+
return deserializeArchivedTeam(readFileSync(filePath, "utf-8"));
|
|
173
|
+
} catch {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Load all archived team snapshots from disk.
|
|
180
|
+
*
|
|
181
|
+
* Malformed files are skipped rather than crashing archive discovery.
|
|
182
|
+
* Results are sorted newest-first for status listings.
|
|
183
|
+
*
|
|
184
|
+
* @returns Archived team snapshots persisted on disk
|
|
185
|
+
*/
|
|
186
|
+
export function loadAllArchivedTeamsFromDisk(): ArchivedTeam[] {
|
|
187
|
+
const dir = ensureTeamArchivesDir();
|
|
188
|
+
const archives: ArchivedTeam[] = [];
|
|
189
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
190
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
191
|
+
try {
|
|
192
|
+
const archive = deserializeArchivedTeam(readFileSync(join(dir, entry.name), "utf-8"));
|
|
193
|
+
if (!archive) continue;
|
|
194
|
+
archives.push(archive);
|
|
195
|
+
} catch {
|
|
196
|
+
// Skip unreadable archive files instead of breaking discovery.
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return archives.sort((left, right) => right.archivedAt - left.archivedAt);
|
|
200
|
+
}
|
|
@@ -10,70 +10,100 @@ import {
|
|
|
10
10
|
AuthStorage,
|
|
11
11
|
createAgentSession,
|
|
12
12
|
createExtensionRuntime,
|
|
13
|
+
DefaultResourceLoader,
|
|
13
14
|
ModelRegistry,
|
|
14
|
-
type ResourceLoader,
|
|
15
15
|
SessionManager,
|
|
16
16
|
SettingsManager,
|
|
17
17
|
} from "@mariozechner/pi-coding-agent";
|
|
18
|
-
import {
|
|
18
|
+
import { applyKnownModelMetadataOverrides } from "../../../src/model-metadata-overrides.js";
|
|
19
|
+
import { getTallowHomeDir, getTallowPath } from "../../_shared/tallow-paths.js";
|
|
20
|
+
import {
|
|
21
|
+
type AgentConfig,
|
|
22
|
+
computeEffectiveTools,
|
|
23
|
+
discoverAgents,
|
|
24
|
+
resolveAgentForExecution,
|
|
25
|
+
} from "../../subagent-tool/agents.js";
|
|
19
26
|
import { type RoutingHints, routeModel } from "../../subagent-tool/model-router.js";
|
|
20
27
|
import { resolveStandardTools } from "../state/team-view.js";
|
|
21
28
|
import type { Teammate } from "../state/types.js";
|
|
22
29
|
import type { Team } from "../store.js";
|
|
23
30
|
import { createTeammateTools } from "../tools/teammate-tools.js";
|
|
24
31
|
|
|
32
|
+
interface SpawnTeammateSessionOptions {
|
|
33
|
+
readonly agentName?: string;
|
|
34
|
+
readonly cwd: string;
|
|
35
|
+
readonly hints?: RoutingHints;
|
|
36
|
+
readonly modelOverride?: string;
|
|
37
|
+
readonly name: string;
|
|
38
|
+
readonly parentModelId?: string;
|
|
39
|
+
readonly piEvents?: ExtensionAPI["events"];
|
|
40
|
+
readonly role?: string;
|
|
41
|
+
readonly team: Team<Teammate>;
|
|
42
|
+
readonly thinkingLevel?: string;
|
|
43
|
+
readonly toolNames?: string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type TeammateThinkingLevel = "high" | "low" | "medium" | "off";
|
|
47
|
+
|
|
25
48
|
/**
|
|
26
|
-
*
|
|
49
|
+
* Coerce an arbitrary string into a supported teammate thinking level.
|
|
27
50
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* - modelScope set → auto-route within that model family
|
|
31
|
-
* - neither → full auto-route based on role complexity and cost preference
|
|
32
|
-
*
|
|
33
|
-
* @param cwd - Working directory
|
|
34
|
-
* @param team - Team to add the teammate to
|
|
35
|
-
* @param name - Teammate name
|
|
36
|
-
* @param role - Role description (used for task classification + system prompt)
|
|
37
|
-
* @param modelOverride - Explicit model name (fuzzy matched). Skips auto-routing.
|
|
38
|
-
* @param toolNames - Standard tool names (defaults to all coding tools)
|
|
39
|
-
* @param piEvents - Event emitter for lifecycle events
|
|
40
|
-
* @param hints - Optional routing hints (modelScope, costPreference, etc.)
|
|
41
|
-
* @param parentModelId - Parent model ID for fallback inheritance
|
|
42
|
-
* @returns The created Teammate
|
|
43
|
-
* @throws If model not found or session creation fails
|
|
51
|
+
* @param value - Raw level string from caller context or tool params
|
|
52
|
+
* @returns Supported thinking level, or undefined when unsupported
|
|
44
53
|
*/
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
): Promise<Teammate> {
|
|
56
|
-
const routing = await routeModel(role, modelOverride, undefined, parentModelId, role, hints, cwd);
|
|
57
|
-
if (!routing.ok) {
|
|
58
|
-
const available = listAvailableModels().slice(0, 20).join(", ");
|
|
59
|
-
throw new Error(`Model not found: "${routing.query}". Available: ${available}`);
|
|
54
|
+
function coerceThinkingLevel(value: string | undefined): TeammateThinkingLevel | undefined {
|
|
55
|
+
if (!value) return undefined;
|
|
56
|
+
const normalized = value.trim().toLowerCase();
|
|
57
|
+
if (
|
|
58
|
+
normalized === "off" ||
|
|
59
|
+
normalized === "low" ||
|
|
60
|
+
normalized === "medium" ||
|
|
61
|
+
normalized === "high"
|
|
62
|
+
) {
|
|
63
|
+
return normalized;
|
|
60
64
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// Use the user's tallow auth and model config so teammates inherit
|
|
64
|
-
// API keys and custom model definitions from the main session.
|
|
65
|
-
const authStorage = AuthStorage.create(getTallowPath("auth.json"));
|
|
66
|
-
const modelRegistry = new ModelRegistry(authStorage, getTallowPath("models.json"));
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Resolve an optional teammate agent template from the shared agent directories.
|
|
70
|
+
*
|
|
71
|
+
* Team teammates should not silently fall back to an ephemeral template when the
|
|
72
|
+
* caller explicitly requested a named agent. Typos must fail closed.
|
|
73
|
+
*
|
|
74
|
+
* @param cwd - Working directory used for project-agent discovery
|
|
75
|
+
* @param agentName - Optional template name requested by the caller
|
|
76
|
+
* @returns Resolved agent template, or undefined when no template was requested
|
|
77
|
+
* @throws {Error} When a named template cannot be found
|
|
78
|
+
*/
|
|
79
|
+
function resolveTeammateAgentTemplate(
|
|
80
|
+
cwd: string,
|
|
81
|
+
agentName: string | undefined
|
|
82
|
+
): AgentConfig | undefined {
|
|
83
|
+
if (!agentName) return undefined;
|
|
84
|
+
const discovery = discoverAgents(cwd, "both");
|
|
85
|
+
const resolved = resolveAgentForExecution(agentName, discovery.agents, discovery.defaults);
|
|
86
|
+
if (resolved.resolution === "ephemeral") {
|
|
70
87
|
throw new Error(
|
|
71
|
-
`
|
|
88
|
+
`Teammate agent template "${agentName}" was not found in user or project agent directories.`
|
|
72
89
|
);
|
|
73
90
|
}
|
|
91
|
+
return resolved.agent;
|
|
92
|
+
}
|
|
74
93
|
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Build the coordination prompt appended to every teammate.
|
|
96
|
+
*
|
|
97
|
+
* @param team - Runtime team container
|
|
98
|
+
* @param name - Teammate name
|
|
99
|
+
* @param role - Effective teammate role text
|
|
100
|
+
* @returns Coordination instructions shared by all teammates
|
|
101
|
+
*/
|
|
102
|
+
function buildCoordinationPrompt(team: Team<Teammate>, name: string, role: string): string {
|
|
103
|
+
const otherNames = Array.from(team.teammates.keys()).filter(
|
|
104
|
+
(teammateName) => teammateName !== name
|
|
105
|
+
);
|
|
106
|
+
return [
|
|
77
107
|
`You are "${name}", a teammate in team "${team.name}".`,
|
|
78
108
|
`Your role: ${role}`,
|
|
79
109
|
"",
|
|
@@ -95,40 +125,183 @@ export async function spawnTeammateSession(
|
|
|
95
125
|
"",
|
|
96
126
|
"Communicate with teammates via team_message when you need their input.",
|
|
97
127
|
].join("\n");
|
|
128
|
+
}
|
|
98
129
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Merge teammate coordination with an optional agent template system prompt.
|
|
132
|
+
*
|
|
133
|
+
* @param team - Runtime team container
|
|
134
|
+
* @param name - Teammate name
|
|
135
|
+
* @param role - Effective teammate role text
|
|
136
|
+
* @param templateAgent - Optional resolved agent template
|
|
137
|
+
* @returns Final system prompt text for the teammate session
|
|
138
|
+
*/
|
|
139
|
+
function buildTeammateSystemPrompt(
|
|
140
|
+
team: Team<Teammate>,
|
|
141
|
+
name: string,
|
|
142
|
+
role: string,
|
|
143
|
+
templateAgent: AgentConfig | undefined
|
|
144
|
+
): string {
|
|
145
|
+
const sections = [buildCoordinationPrompt(team, name, role)];
|
|
146
|
+
if (templateAgent?.systemPrompt.trim()) {
|
|
147
|
+
sections.push(
|
|
148
|
+
[`Base agent template: ${templateAgent.name}`, templateAgent.systemPrompt.trim()].join("\n\n")
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
if (templateAgent?.maxTurns) {
|
|
152
|
+
sections.unshift(
|
|
153
|
+
`You have a maximum of ${templateAgent.maxTurns} tool-use turns for this task. Plan accordingly and return your best partial result before hitting the limit.`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return sections.join("\n\n");
|
|
157
|
+
}
|
|
111
158
|
|
|
112
|
-
|
|
159
|
+
/**
|
|
160
|
+
* Resolve the standard tool allowlist for a teammate.
|
|
161
|
+
*
|
|
162
|
+
* Explicit tool names passed to `team_spawn` win. Otherwise agent-template
|
|
163
|
+
* allow/deny lists are applied when present.
|
|
164
|
+
*
|
|
165
|
+
* @param explicitToolNames - Tools passed directly to team_spawn
|
|
166
|
+
* @param templateAgent - Optional resolved agent template
|
|
167
|
+
* @returns Effective tool names, or undefined to allow the default coding set
|
|
168
|
+
*/
|
|
169
|
+
function resolveTeammateToolNames(
|
|
170
|
+
explicitToolNames: string[] | undefined,
|
|
171
|
+
templateAgent: AgentConfig | undefined
|
|
172
|
+
): string[] | undefined {
|
|
173
|
+
if (explicitToolNames) return explicitToolNames;
|
|
174
|
+
if (!templateAgent) return undefined;
|
|
175
|
+
return computeEffectiveTools(templateAgent.tools, templateAgent.disallowedTools);
|
|
176
|
+
}
|
|
113
177
|
|
|
114
|
-
|
|
178
|
+
/**
|
|
179
|
+
* Create a loader that only exposes the requested teammate skills/system prompt.
|
|
180
|
+
*
|
|
181
|
+
* Extensions are disabled on purpose — teammates get only the explicitly passed
|
|
182
|
+
* standard tools plus the injected team coordination tools.
|
|
183
|
+
*
|
|
184
|
+
* @param cwd - Working directory
|
|
185
|
+
* @param settingsManager - Shared in-memory settings for the teammate session
|
|
186
|
+
* @param systemPrompt - Final system prompt text
|
|
187
|
+
* @param templateAgent - Optional resolved agent template
|
|
188
|
+
* @returns Reloaded resource loader ready for createAgentSession
|
|
189
|
+
*/
|
|
190
|
+
async function createTeammateResourceLoader(
|
|
191
|
+
cwd: string,
|
|
192
|
+
settingsManager: SettingsManager,
|
|
193
|
+
systemPrompt: string,
|
|
194
|
+
templateAgent: AgentConfig | undefined
|
|
195
|
+
): Promise<DefaultResourceLoader> {
|
|
196
|
+
const requestedSkills = new Set(templateAgent?.skills ?? []);
|
|
197
|
+
const loader = new DefaultResourceLoader({
|
|
198
|
+
agentDir: getTallowHomeDir(),
|
|
115
199
|
cwd,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
200
|
+
extensionsOverride: () => ({ extensions: [], errors: [], runtime: createExtensionRuntime() }),
|
|
201
|
+
promptsOverride: () => ({ diagnostics: [], prompts: [] }),
|
|
202
|
+
settingsManager,
|
|
203
|
+
skillsOverride: (base) => ({
|
|
204
|
+
diagnostics: base.diagnostics,
|
|
205
|
+
skills:
|
|
206
|
+
requestedSkills.size === 0
|
|
207
|
+
? []
|
|
208
|
+
: base.skills.filter((skill) => requestedSkills.has(skill.name)),
|
|
209
|
+
}),
|
|
210
|
+
systemPromptOverride: () => systemPrompt,
|
|
211
|
+
});
|
|
212
|
+
await loader.reload();
|
|
213
|
+
return loader;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Spawn a teammate as an in-process AgentSession with shared team tools.
|
|
218
|
+
*
|
|
219
|
+
* Model selection follows the same routing as subagents:
|
|
220
|
+
* - modelOverride set → explicit fuzzy resolution (best match)
|
|
221
|
+
* - template agent model set → explicit or auto-routing keyword from frontmatter
|
|
222
|
+
* - neither → full auto-route based on role complexity and cost preference
|
|
223
|
+
*
|
|
224
|
+
* @param options - Session spawn options
|
|
225
|
+
* @returns The created Teammate
|
|
226
|
+
* @throws {Error} If model or agent template resolution fails
|
|
227
|
+
*/
|
|
228
|
+
export async function spawnTeammateSession(
|
|
229
|
+
options: SpawnTeammateSessionOptions
|
|
230
|
+
): Promise<Teammate> {
|
|
231
|
+
const templateAgent = resolveTeammateAgentTemplate(options.cwd, options.agentName);
|
|
232
|
+
const role = options.role?.trim() || templateAgent?.description?.trim();
|
|
233
|
+
if (!role) {
|
|
234
|
+
throw new Error("team_spawn requires either a role or an agent template.");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const routingTask = templateAgent?.systemPrompt?.trim()
|
|
238
|
+
? `${role}\n\n${templateAgent.systemPrompt.trim()}`
|
|
239
|
+
: role;
|
|
240
|
+
const routing = await routeModel(
|
|
241
|
+
routingTask,
|
|
242
|
+
options.modelOverride,
|
|
243
|
+
templateAgent?.model,
|
|
244
|
+
options.parentModelId,
|
|
245
|
+
role,
|
|
246
|
+
options.hints,
|
|
247
|
+
options.cwd
|
|
248
|
+
);
|
|
249
|
+
if (!routing.ok) {
|
|
250
|
+
const available = listAvailableModels().slice(0, 20).join(", ");
|
|
251
|
+
throw new Error(`Model not found: "${routing.query}". Available: ${available}`);
|
|
252
|
+
}
|
|
253
|
+
const resolvedModel = routing.model;
|
|
254
|
+
|
|
255
|
+
// Use the user's tallow auth and model config so teammates inherit
|
|
256
|
+
// API keys and custom model definitions from the main session.
|
|
257
|
+
const authStorage = AuthStorage.create(getTallowPath("auth.json"));
|
|
258
|
+
const modelRegistry = new ModelRegistry(authStorage, getTallowPath("models.json"));
|
|
259
|
+
applyKnownModelMetadataOverrides(modelRegistry);
|
|
260
|
+
const model = modelRegistry.find(resolvedModel.provider, resolvedModel.id);
|
|
261
|
+
if (!model) {
|
|
262
|
+
throw new Error(
|
|
263
|
+
`Model resolved to "${resolvedModel.id}" (provider: ${resolvedModel.provider}) but not found in registry`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const systemPrompt = buildTeammateSystemPrompt(options.team, options.name, role, templateAgent);
|
|
268
|
+
const settingsManager = SettingsManager.inMemory({
|
|
269
|
+
compaction: { enabled: true },
|
|
270
|
+
retry: { enabled: true, maxRetries: 2 },
|
|
271
|
+
});
|
|
272
|
+
const resourceLoader = await createTeammateResourceLoader(
|
|
273
|
+
options.cwd,
|
|
274
|
+
settingsManager,
|
|
275
|
+
systemPrompt,
|
|
276
|
+
templateAgent
|
|
277
|
+
);
|
|
278
|
+
const teammateCustomTools = createTeammateTools(options.team, options.name, options.piEvents);
|
|
279
|
+
const thinkingLevel = coerceThinkingLevel(options.thinkingLevel) ?? "off";
|
|
280
|
+
const toolNames = resolveTeammateToolNames(options.toolNames, templateAgent);
|
|
281
|
+
const standardTools =
|
|
282
|
+
toolNames && toolNames.length === 0 ? [] : resolveStandardTools(options.cwd, toolNames);
|
|
283
|
+
|
|
284
|
+
const { session } = await createAgentSession({
|
|
119
285
|
authStorage,
|
|
286
|
+
customTools: teammateCustomTools,
|
|
287
|
+
cwd: options.cwd,
|
|
288
|
+
agentDir: path.join(os.tmpdir(), `pi-team-${options.team.name}-${options.name}`),
|
|
289
|
+
model,
|
|
120
290
|
modelRegistry,
|
|
121
291
|
resourceLoader,
|
|
122
|
-
tools: resolveStandardTools(cwd, toolNames),
|
|
123
|
-
customTools: teammateCustomTools,
|
|
124
292
|
sessionManager: SessionManager.inMemory(),
|
|
125
|
-
settingsManager
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}),
|
|
293
|
+
settingsManager,
|
|
294
|
+
thinkingLevel,
|
|
295
|
+
tools: standardTools,
|
|
129
296
|
});
|
|
130
297
|
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
298
|
+
const teammate: Teammate = {
|
|
299
|
+
model: resolvedModel.id,
|
|
300
|
+
name: options.name,
|
|
301
|
+
role,
|
|
302
|
+
session,
|
|
303
|
+
status: "idle",
|
|
304
|
+
};
|
|
305
|
+
options.team.teammates.set(options.name, teammate);
|
|
306
|
+
return teammate;
|
|
134
307
|
}
|