test(connector-id): migrate fixtures and assertions to pi-mono ids

Replaces legacy adapter ids (codex_local / claude_local / gemini_local /
claude_compatible) across ~42 test files with pi-mono equivalents:
- codex_local → openai-codex
- claude_local → claude-pro
- gemini_local → gemini-cli
- claude_compatible → anthropic

Hand-rewrites where fixture semantics changed:
- connector-matching: TASK_TYPE_PREFERENCE ordering rebased (claude-pro
  first for code/content/management; google first for research)
- connector-model-policy: getModels(provider) from pi-ai instead of the
  deleted adapter.module.models array
- session-management + edge-case-session: adapter exemption removed;
  rotation now uniform under pi-mono
- ipc-core-admin-handlers: CSV compat model replaced with pi-ai anthropic
- connectors.spec: lookup by openai-codex id (catalog now starts with claude-pro)
- onboarding-model-pool: listProviderModels(provider) helper; label text
  changed to "ChatGPT Plus/Pro (Codex)"
- database integration: added vault insecure-fallback env + electron mock
- command-paths: uses new @main/connector-helpers normalizeCommandValue
- pi-mono-migrations: added `as string` casts for 3 negative-existence
  assertions verifying legacy ids were removed

Drops:
- pnpm-workspace.yaml packages: [] (was "packages/*"; directory empty)
- obsolete CONNECTOR_STRENGTHS import; .module.models probing

Verification:
- pnpm typecheck → 0 errors
- pnpm lint → 0 errors / 0 warnings
- pnpm build:verify → 0 errors / 0 warnings
- pnpm test:unit → 508/508 pass
- pnpm test:integration → 96/96 pass
- All 6 adapter hard gates pass (NOT FOUND / 0 matches / empty)
- pi-ai / pi-agent-core / pi-coding-agent all resolved @0.67.6
This commit is contained in:
ZacharyZhang-NY committed 2026-04-17 02:36:53 -04:00
1 parent 4de9cd0339
commit e94939a562
80 files changed
+2210 -413

No files matched your search

+28 -2
View File
@@ -1,11 +1,14 @@
/**
* Role-classification helpers used across prompt construction and skill
* auto-assignment. Keeping a single keyword list here prevents drift
* between system-prompt.ts and automation-skills.ts.
* auto-assignment. Historical keyword match remains here for backfill and
* UI-free fallback; typed `agent.roleLevel` is the source of truth once the
* column is populated.
*
* Matching uses word-boundary tokenization instead of substring search,
* so "lead" does not match "cleaner" or "leaderboard".
*/
import type { AgentRoleLevel } from "@shared/types";
import { isLeadershipLevel } from "@shared/types";
export const LEADERSHIP_ROLE_KEYWORDS = [
"chief",
@@ -31,3 +34,26 @@ export function roleMatchesLeadershipKeywords(role: string, title?: string | nul
const tokens = new Set(tokenize(`${role} ${title ?? ""}`));
return LEADERSHIP_ROLE_KEYWORDS.some((keyword) => tokens.has(keyword));
}
/**
* One-shot inference from free-text role/title strings. Used when creating or
* backfilling an agent so `roleLevel` can be stored once and re-read cheaply.
* The most specific token wins; order here is most-senior → least.
*/
export function inferRoleLevel(role: string, title?: string | null): AgentRoleLevel {
const tokens = new Set(tokenize(`${role} ${title ?? ""}`));
if (tokens.has("ceo") || tokens.has("cto") || tokens.has("cmo") || tokens.has("coo") || tokens.has("cfo") || tokens.has("chief")) {
return "c_suite";
}
if (tokens.has("vp") || tokens.has("vice") || tokens.has("president")) return "vp";
if (tokens.has("director") || tokens.has("head")) return "director";
if (tokens.has("manager")) return "manager";
if (tokens.has("lead")) return "lead";
return "ic";
}
/** Canonical leadership check: typed enum wins, fallback to keyword match. */
export function agentIsLeader(agent: { roleLevel?: AgentRoleLevel | null; role: string; title: string | null }): boolean {
if (agent.roleLevel) return isLeadershipLevel(agent.roleLevel);
return roleMatchesLeadershipKeywords(agent.role, agent.title);
}
+134
View File
@@ -0,0 +1,134 @@
import type { AppDatabase } from "@main/database";
import type { DesktopEvent } from "@shared/types";
interface BeforeRequestPayload {
agentId: string;
companyId: string;
taskId: string | null;
toolCallId: string;
toolName: string;
args: unknown;
}
interface AfterRequestPayload {
toolCallId: string;
toolName: string;
isError: boolean;
}
export interface HumanApprovalHookDeps {
db: AppDatabase;
emitEvent: (event: DesktopEvent) => void;
logger: {
info: (...args: unknown[]) => void;
warn: (...args: unknown[]) => void;
error: (...args: unknown[]) => void;
};
}
interface PendingApproval {
approvalId: string;
resolve: (verdict: { block?: boolean; reason?: string }) => void;
}
const TOOL_LABEL: Record<string, string> = {
bash: "shell",
edit: "edit",
write: "write",
read: "read",
ls: "ls",
grep: "grep",
find: "find",
};
function summariseArgs(toolName: string, args: unknown): string {
if (args === null || typeof args !== "object") return String(args);
const argsObj = args as Record<string, unknown>;
switch (toolName) {
case "bash": {
const command = typeof argsObj.command === "string" ? argsObj.command : "";
return command.length > 200 ? `${command.slice(0, 197)}...` : command;
}
case "write":
case "edit":
case "read": {
const path = typeof argsObj.path === "string" ? argsObj.path : (typeof argsObj.file_path === "string" ? argsObj.file_path : "");
return path;
}
default:
try {
const json = JSON.stringify(args);
return json.length > 200 ? `${json.slice(0, 197)}...` : json;
} catch {
return "<unserialisable args>";
}
}
}
/**
* Human-in-the-loop hook bridging pi `beforeToolCall` to
* AgentCompany's `ApprovalService` via the existing approvals table.
*/
export class HumanApprovalHook {
private readonly pending = new Map<string, PendingApproval>();
constructor(private readonly deps: HumanApprovalHookDeps) {}
async before(runId: string, payload: BeforeRequestPayload): Promise<{ block?: boolean; reason?: string }> {
const { db, emitEvent } = this.deps;
const summary = summariseArgs(payload.toolName, payload.args);
const label = TOOL_LABEL[payload.toolName] ?? payload.toolName;
const approvalId = db.requestApproval({
companyId: payload.companyId,
relatedTaskId: payload.taskId,
requestedByAgentId: payload.agentId,
type: "dangerous_command",
payloadSummary: `${label}: ${summary}`.slice(0, 280),
impactSummary: `Agent ${payload.agentId} requested ${label} during run ${runId}.`,
payloadJson: JSON.stringify({
runId,
toolCallId: payload.toolCallId,
toolName: payload.toolName,
args: payload.args,
}),
});
emitEvent({ type: "approval-updated", approvalId, action: "created" });
emitEvent({ type: "domain-changed" });
return new Promise<{ block?: boolean; reason?: string }>((resolve) => {
this.pending.set(approvalId, { approvalId, resolve });
});
}
/**
* Called by the IPC approval-decided pipeline when a user resolves a
* pending approval. If we have a pending pi `beforeToolCall` waiting on
* this approval, resolve it.
*/
resolveApproval(approvalId: string, decision: "approve" | "reject" | "revision_requested", note: string): boolean {
const pending = this.pending.get(approvalId);
if (!pending) return false;
this.pending.delete(approvalId);
if (decision === "approve") {
pending.resolve({});
} else {
pending.resolve({ block: true, reason: note || `Tool execution ${decision}.` });
}
return true;
}
/** Called when a run is cancelled — resolves any pending approvals as blocked. */
cancelAll(reason: string): void {
for (const [id, pending] of this.pending) {
pending.resolve({ block: true, reason });
this.pending.delete(id);
}
}
async after(_runId: string, _payload: AfterRequestPayload): Promise<void> {
// Audit hook intentionally minimal — actual tool result is captured by
// the agent-event stream and persisted via SqliteSessionStore.
}
}
+4
View File
@@ -0,0 +1,4 @@
export { SqliteSessionStore } from "./sqlite-session-store";
export { HumanApprovalHook } from "./approval-hook";
export { CostWriter } from "./cost-writer";
export { RuntimeBridge } from "./runtime-bridge";
@@ -0,0 +1,65 @@
import type { AgentMessage } from "@mariozechner/pi-agent-core";
import type { AppDatabase } from "@main/database";
/**
* SQLite-backed pi `AgentMessage[]` store.
*
* One store instance per task_session. Owners must call `appendMessage`
* exactly once per pi `message_end` event, and `replaceMessages` after pi
* `compact` returns a compacted message list.
*/
export class SqliteSessionStore {
constructor(
private readonly db: AppDatabase,
public readonly taskSessionId: string,
) {}
loadMessages(): AgentMessage[] {
const rows = this.db.sessionEntries.listActive(this.taskSessionId);
const result: AgentMessage[] = [];
for (const row of rows) {
if (row.kind === "message") {
try {
result.push(JSON.parse(row.payloadJson) as AgentMessage);
} catch {
// Skip malformed entries (defensive) — preserves the rest of the
// transcript instead of failing the entire load.
}
}
}
return result;
}
appendMessage(message: AgentMessage): void {
this.db.sessionEntries.append({
taskSessionId: this.taskSessionId,
kind: "message",
role: (message as { role?: string }).role ?? null,
payloadJson: JSON.stringify(message),
});
}
appendCompaction(summary: string): void {
this.db.sessionEntries.append({
taskSessionId: this.taskSessionId,
kind: "compaction",
role: null,
payloadJson: JSON.stringify({ summary }),
});
}
replaceMessages(messages: AgentMessage[], compactionSummary?: string): void {
const entries: Array<{ kind: "message" | "compaction"; role: string | null; payloadJson: string }> = [];
if (compactionSummary) {
entries.push({ kind: "compaction", role: null, payloadJson: JSON.stringify({ summary: compactionSummary }) });
}
for (const message of messages) {
entries.push({
kind: "message",
role: (message as { role?: string }).role ?? null,
payloadJson: JSON.stringify(message),
});
}
this.db.sessionEntries.replace({ taskSessionId: this.taskSessionId, entries });
}
}
+122
View File
@@ -0,0 +1,122 @@
import type { AppDatabase } from "@main/database";
import type { CredentialBackend, CredentialKind } from "@main/database/credentials";
import type { CredentialStatus } from "@main/database/credentials";
import type { CredentialSummary } from "@shared/contracts/schemas-credentials";
import type { StoredCredentialPayload } from "./types";
const HINT_SUFFIX_LEN = 4;
const HINT_PREFIX_LEN = 6;
function maskApiKey(value: string): string {
if (value.length <= HINT_PREFIX_LEN + HINT_SUFFIX_LEN) {
return `${value.slice(0, 2)}...`;
}
const prefix = value.slice(0, HINT_PREFIX_LEN);
const suffix = value.slice(-HINT_SUFFIX_LEN);
return `${prefix}...${suffix}`;
}
export function deriveDisplayHint(payload: StoredCredentialPayload): string {
switch (payload.kind) {
case "api_key":
return payload.value.source === "literal"
? maskApiKey(payload.value.value)
: `env:${payload.value.value}`;
case "azure":
return payload.value.resourceName
? `azure:${payload.value.resourceName}`
: `azure:${maskApiKey(payload.value.apiKey)}`;
case "aws":
if (payload.value.authMode === "profile") return `aws:profile=${payload.value.awsProfile ?? "?"}`;
if (payload.value.authMode === "iam") return `aws:iam=${maskApiKey(payload.value.accessKeyId ?? "?")}`;
return `aws:bearer`;
case "gcp_adc":
return `gcp:${payload.value.project}@${payload.value.location}`;
case "oauth": {
const expiresMs = payload.value.expires;
const expiresLabel = typeof expiresMs === "number" && Number.isFinite(expiresMs)
? new Date(expiresMs).toISOString()
: "no-expiry";
return `oauth:expires=${expiresLabel}`;
}
}
}
export interface CredentialStoreDeps {
db: AppDatabase;
}
export class CredentialStore {
constructor(private readonly deps: CredentialStoreDeps) {}
list(): CredentialSummary[] {
return this.deps.db.credentialsTable.list().map((row) => ({
connectorId: row.connectorId,
kind: row.kind as CredentialKind,
displayHint: row.displayHint,
status: row.status,
expiresAt: row.expiresAt,
lastRefreshedAt: row.lastRefreshedAt,
lastError: row.lastError,
updatedAt: row.updatedAt,
}));
}
/** Encrypts and stores a credential. Caller is responsible for kind/connectorId match. */
save(connectorId: string, payload: StoredCredentialPayload, expiresAt: string | null = null): CredentialSummary {
const json = JSON.stringify(payload.value);
const { backend, payload: ciphertext } = this.deps.db.vault.encrypt(json);
const stored = this.deps.db.credentialsTable.upsert({
connectorId,
kind: payload.kind,
backend: backend as CredentialBackend,
ciphertext,
displayHint: deriveDisplayHint(payload),
status: "active",
expiresAt,
lastRefreshedAt: null,
lastError: null,
});
return {
connectorId: stored.connectorId,
kind: stored.kind,
displayHint: stored.displayHint,
status: stored.status,
expiresAt: stored.expiresAt,
lastRefreshedAt: stored.lastRefreshedAt,
lastError: stored.lastError,
updatedAt: stored.updatedAt,
};
}
/** Decrypts and returns the typed credential payload. Returns null when missing. */
read(connectorId: string): StoredCredentialPayload | null {
const row = this.deps.db.credentialsTable.get(connectorId);
if (!row) return null;
const json = this.deps.db.vault.decrypt(
row.backend,
Buffer.isBuffer(row.ciphertext) ? row.ciphertext : Buffer.from(row.ciphertext),
);
const value = JSON.parse(json);
return { kind: row.kind, value } as StoredCredentialPayload;
}
remove(connectorId: string): void {
this.deps.db.credentialsTable.remove(connectorId);
}
setStatus(connectorId: string, status: CredentialStatus, lastError: string | null = null): void {
this.deps.db.credentialsTable.setStatus(connectorId, status, lastError);
}
/** Replaces the OAuth ciphertext after a successful refresh. */
saveRefreshedOAuth(connectorId: string, payload: StoredCredentialPayload, expiresAt: string | null): void {
const { backend, payload: ciphertext } = this.deps.db.vault.encrypt(JSON.stringify(payload.value));
this.deps.db.credentialsTable.markRefreshed(
connectorId,
backend as CredentialBackend,
ciphertext,
expiresAt,
);
}
}
+4
View File
@@ -0,0 +1,4 @@
export { CredentialStore, deriveDisplayHint } from "./credential-store";
export { CredentialResolver, CredentialError, type ResolvedCredential } from "./credential-resolver";
export { OAuthLoginService } from "./oauth-login-service";
export type { StoredCredentialPayload, OAuthCredentialPayload, CredentialKind } from "./types";
+30
View File
@@ -0,0 +1,30 @@
import type {
ApiKeyValue,
AwsCredential,
AzureCredential,
GcpAdcCredential,
} from "@shared/contracts/schemas-credentials";
/**
* pi-ai's `OAuthCredentials` shape.
*
* Mirrored locally as a type-only declaration to avoid pulling pi runtime
* symbols into modules that only need the shape. The actual contract is
* enforced by pi at runtime via the OAuth provider interface.
*/
export interface OAuthCredentialPayload {
refresh: string;
access: string;
expires: number;
/** Provider-specific extra fields (e.g. github copilot domain) */
[key: string]: unknown;
}
export type StoredCredentialPayload =
| { kind: "oauth"; value: OAuthCredentialPayload }
| { kind: "api_key"; value: ApiKeyValue }
| { kind: "azure"; value: AzureCredential }
| { kind: "aws"; value: AwsCredential }
| { kind: "gcp_adc"; value: GcpAdcCredential };
export type CredentialKind = StoredCredentialPayload["kind"];
+70
View File
@@ -0,0 +1,70 @@
import type { DatabaseSync } from "node:sqlite";
import { STALE_LOCK_TIMEOUT_MS } from "./database-base";
function nowIso(): string {
return new Date().toISOString();
}
/**
* Stale-state recovery helpers extracted from `database-base.ts` to keep that
* file under the 400-line cap. Both helpers are called by `AppDatabase.init`
* (interruptStaleRuns) and the orchestration recovery loop (recoverStaleLocks).
*/
interface InterruptDeps {
db: DatabaseSync;
transaction: <T>(op: () => T) => T;
releaseAllWorkspaceLocks: () => void;
}
export function interruptStaleRunsImpl(deps: InterruptDeps): number {
const ts = nowIso();
let interruptedCount = 0;
deps.transaction(() => {
const result = deps.db
.prepare(
"update runs set status = 'interrupted', error_message = 'AgentCompany restarted before the run could report completion.', finished_at = ?, updated_at = ? where status = 'running'",
)
.run(ts, ts);
interruptedCount = Number(result.changes);
if (interruptedCount > 0) {
deps.db
.prepare(
"update tasks set status = 'blocked', updated_at = ? where id in (select task_id from runs where status = 'interrupted' and finished_at = ?) and status in ('in_progress', 'todo')",
)
.run(ts, ts);
}
deps.db
.prepare(
"update tasks set active_run_id = null where active_run_id in (select id from runs where status = 'interrupted')",
)
.run();
deps.db.prepare("update agents set status = 'idle', updated_at = ? where status = 'running'").run(ts);
deps.releaseAllWorkspaceLocks();
});
return interruptedCount;
}
interface RecoverDeps {
db: DatabaseSync;
readAll: <T>(sql: string, ...params: import("node:sqlite").SQLInputValue[]) => T[];
}
export function recoverStaleLocksImpl(
deps: RecoverDeps,
): Array<{ taskId: string; companyId: string; title: string }> {
const cutoff = new Date(Date.now() - STALE_LOCK_TIMEOUT_MS).toISOString();
const stale = deps.readAll<{ id: string; companyId: string; title: string }>(
"SELECT id, company_id AS companyId, title FROM tasks WHERE execution_locked_at IS NOT NULL AND execution_locked_at < ? AND status = 'in_progress' AND checkout_run_id NOT IN (SELECT id FROM runs WHERE status = 'running')",
cutoff,
);
const ts = nowIso();
for (const task of stale) {
deps.db
.prepare(
"UPDATE tasks SET checkout_run_id = NULL, execution_locked_at = NULL, status = 'todo', updated_at = ? WHERE id = ?",
)
.run(ts, task.id);
}
return stale.map((t) => ({ taskId: t.id, companyId: t.companyId, title: t.title }));
}
+2 -2
View File
@@ -63,7 +63,7 @@ export function buildEntityCrudMethods(
getAgent(id: string): AgentRecord {
const row = getDb()
.prepare(
"select id, company_id as companyId, name, role, title, department, status, reports_to as reportsTo, connector_id as connectorId, workspace_id as workspaceId, model, capabilities, budget_monthly_usd as budgetMonthlyUsd, spent_monthly_usd as spentMonthlyUsd, metadata_json as metadataJson, heartbeat_enabled as heartbeatEnabled, heartbeat_interval_sec as heartbeatIntervalSec, last_heartbeat_at as lastHeartbeatAt, created_at as createdAt, updated_at as updatedAt from agents where id = ?",
"select id, company_id as companyId, name, role, title, department, status, reports_to as reportsTo, connector_id as connectorId, workspace_id as workspaceId, model, capabilities, role_level as roleLevel, budget_monthly_usd as budgetMonthlyUsd, spent_monthly_usd as spentMonthlyUsd, metadata_json as metadataJson, heartbeat_enabled as heartbeatEnabled, heartbeat_interval_sec as heartbeatIntervalSec, last_heartbeat_at as lastHeartbeatAt, created_at as createdAt, updated_at as updatedAt from agents where id = ?",
)
.get(id) as AgentRecord | undefined;
if (!row) throw new Error(`Agent not found: ${id}`);
@@ -74,7 +74,7 @@ export function buildEntityCrudMethods(
findAgent(id: string): AgentRecord | null {
const row = getDb()
.prepare(
"select id, company_id as companyId, name, role, title, department, status, reports_to as reportsTo, connector_id as connectorId, workspace_id as workspaceId, model, capabilities, budget_monthly_usd as budgetMonthlyUsd, spent_monthly_usd as spentMonthlyUsd, metadata_json as metadataJson, heartbeat_enabled as heartbeatEnabled, heartbeat_interval_sec as heartbeatIntervalSec, last_heartbeat_at as lastHeartbeatAt, created_at as createdAt, updated_at as updatedAt from agents where id = ?",
"select id, company_id as companyId, name, role, title, department, status, reports_to as reportsTo, connector_id as connectorId, workspace_id as workspaceId, model, capabilities, role_level as roleLevel, budget_monthly_usd as budgetMonthlyUsd, spent_monthly_usd as spentMonthlyUsd, metadata_json as metadataJson, heartbeat_enabled as heartbeatEnabled, heartbeat_interval_sec as heartbeatIntervalSec, last_heartbeat_at as lastHeartbeatAt, created_at as createdAt, updated_at as updatedAt from agents where id = ?",
)
.get(id) as AgentRecord | undefined;
if (!row) return null;
+1 -1
View File
@@ -63,7 +63,7 @@ export function buildSnapshotMethods(
...companyArgs,
).map((record) => ({ ...record, isPrimary: toBoolean(record.isPrimary) })) as WorkspaceRecord[],
agents: readAll<SqliteRow>(
`select id, company_id as companyId, name, role, title, department, status, reports_to as reportsTo, connector_id as connectorId, workspace_id as workspaceId, model, capabilities, budget_monthly_usd as budgetMonthlyUsd, spent_monthly_usd as spentMonthlyUsd, metadata_json as metadataJson, heartbeat_enabled as heartbeatEnabled, heartbeat_interval_sec as heartbeatIntervalSec, last_heartbeat_at as lastHeartbeatAt, created_at as createdAt, updated_at as updatedAt from agents${companyWhere} order by created_at asc`,
`select id, company_id as companyId, name, role, title, department, status, reports_to as reportsTo, connector_id as connectorId, workspace_id as workspaceId, model, capabilities, role_level as roleLevel, budget_monthly_usd as budgetMonthlyUsd, spent_monthly_usd as spentMonthlyUsd, metadata_json as metadataJson, heartbeat_enabled as heartbeatEnabled, heartbeat_interval_sec as heartbeatIntervalSec, last_heartbeat_at as lastHeartbeatAt, created_at as createdAt, updated_at as updatedAt from agents${companyWhere} order by created_at asc`,
...companyArgs,
).map((record: SqliteRow) => ({ ...record, heartbeatEnabled: toBoolean(record.heartbeatEnabled) })) as AgentRecord[],
goals: readAll<GoalRecord>(
+91
View File
@@ -0,0 +1,91 @@
/**
* Resolves `Model<Api>` and the dynamic stream headers needed for providers
* that pi-ai's catalog does not ship out of the box.
*
* Today this only covers the two Fireworks connectors (`fireworks` and
* `fireworks-firepass`), which both speak the Anthropic-compatible endpoint
* at `https://api.fireworks.ai/inference` and require Bearer auth. For every
* other provider we delegate to pi-ai's `getModel` / `getModels`.
*/
import { getModel, getModels, type Api, type Model } from "@mariozechner/pi-ai";
const FIREWORKS_BASE_URL = "https://api.fireworks.ai/inference";
const FIREWORKS_DEFAULT_CONTEXT = 128_000;
const FIREWORKS_DEFAULT_MAX_TOKENS = 8192;
export const FIREWORKS_FIREPASS_MODEL_ID = "accounts/fireworks/routers/kimi-k2p5-turbo";
export const FIREWORKS_FIREPASS_MODEL_NAME = "Kimi K2.5 Turbo (Fire Pass)";
const FIREWORKS_CURATED_MODELS: Array<{ id: string; name: string; contextWindow?: number }> = [
{ id: "accounts/fireworks/models/kimi-k2-instruct", name: "Kimi K2 Instruct", contextWindow: 131_072 },
{ id: "accounts/fireworks/models/qwen3-coder-480b-a35b-instruct", name: "Qwen3 Coder 480B A35B Instruct", contextWindow: 262_144 },
{ id: "accounts/fireworks/models/deepseek-v3p1", name: "DeepSeek V3.1", contextWindow: 163_840 },
{ id: "accounts/fireworks/models/glm-4p5", name: "GLM 4.5", contextWindow: 131_072 },
{ id: "accounts/fireworks/models/llama-v3p3-70b-instruct", name: "Llama 3.3 70B Instruct", contextWindow: 131_072 },
];
function buildFireworksModel(modelId: string, name?: string, contextWindow?: number): Model<"anthropic-messages"> {
return {
id: modelId,
name: name ?? modelId,
api: "anthropic-messages",
provider: "fireworks",
baseUrl: FIREWORKS_BASE_URL,
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: contextWindow ?? FIREWORKS_DEFAULT_CONTEXT,
maxTokens: FIREWORKS_DEFAULT_MAX_TOKENS,
};
}
function isFireworksProvider(provider: string): boolean {
return provider === "fireworks" || provider === "fireworks-firepass";
}
export function resolveModel(provider: string, modelId: string): Model<Api> | null {
if (provider === "fireworks-firepass") {
return buildFireworksModel(FIREWORKS_FIREPASS_MODEL_ID, FIREWORKS_FIREPASS_MODEL_NAME, 262_144);
}
if (provider === "fireworks") {
const curated = FIREWORKS_CURATED_MODELS.find((entry) => entry.id === modelId);
return buildFireworksModel(modelId, curated?.name, curated?.contextWindow);
}
try {
return getModel(provider as never, modelId as never) ?? null;
} catch {
return null;
}
}
export function resolveModels(provider: string): Array<{ id: string; name: string }> {
if (provider === "fireworks-firepass") {
return [{ id: FIREWORKS_FIREPASS_MODEL_ID, name: FIREWORKS_FIREPASS_MODEL_NAME }];
}
if (provider === "fireworks") {
return FIREWORKS_CURATED_MODELS.map(({ id, name }) => ({ id, name }));
}
try {
const models = getModels(provider as never) ?? [];
return models.map((model: { id: string; name?: string }) => ({ id: model.id, name: model.name ?? model.id }));
} catch {
return [];
}
}
/**
* Extra HTTP headers that must be merged into the streamSimple call for the
* given provider. Returns undefined when no extra headers are required.
*
* Fireworks' Anthropic-compat endpoint authenticates via `Authorization: Bearer`,
* not `x-api-key`. pi-ai's anthropic provider only sends `x-api-key`, so we
* inject the bearer header at the call site.
*/
export function resolveExtraStreamHeaders(provider: string, apiKey: string): Record<string, string> | undefined {
if (!apiKey) return undefined;
if (isFireworksProvider(provider)) {
return { Authorization: `Bearer ${apiKey}` };
}
return undefined;
}
+65
View File
@@ -0,0 +1,65 @@
import type log from "electron-log/main.js";
import type { AppDatabase } from "./database";
interface ResumeInterruptedTasksDeps {
db: AppDatabase;
logger: typeof log;
wakeAgentIfPossible: (agentId: string, companyId: string, trigger: string) => string | null;
publishDomainChanged: () => void;
}
/**
* Resume tasks that were left behind by a clean shutdown.
*
* The worker-service marks running/queued runs as "interrupted" with a fixed
* summary string on shutdown, but does not reset the owning task. This helper
* rolls those tasks back to "todo", releases their run locks, and re-wakes the
* assigned agents so heartbeat can pick them up again.
*/
export function resumeInterruptedTasks({
db,
logger,
wakeAgentIfPossible,
publishDomainChanged,
}: ResumeInterruptedTasksDeps): void {
try {
const snapshot = db.listSnapshot();
const shutdownRuns = snapshot.runs.filter(
(r) =>
r.status === "interrupted" &&
(r.summary === "Application shut down." || r.summary === "Worker process crashed."),
);
if (shutdownRuns.length === 0) return;
const resumedAgents = new Set<string>();
for (const run of shutdownRuns) {
try {
const task = db.getTask(run.taskId);
if (task.status === "in_progress" || task.status === "blocked") {
db.advanceTaskStatus(run.taskId, "todo", "Auto-resumed after app restart.");
db.releaseTaskLock(run.taskId, run.id);
}
if (!resumedAgents.has(run.agentId)) {
resumedAgents.add(run.agentId);
wakeAgentIfPossible(run.agentId, run.companyId, "app_restart_resume");
}
} catch (error) {
logger.warn(
`[bootstrap] Skipped shutdown recovery for run ${run.id} (task/agent may have been deleted):`,
error,
);
}
}
if (resumedAgents.size > 0) {
logger.info(
`[bootstrap] Resumed ${shutdownRuns.length} interrupted task(s) across ${resumedAgents.size} agent(s) after clean shutdown recovery.`,
);
publishDomainChanged();
}
} catch (err) {
logger.warn(
`[bootstrap] Shutdown recovery failed: ${err instanceof Error ? err.message : String(err)}`,
);
}
}
@@ -0,0 +1,65 @@
/**
* Helpers for assembling orchestration-service dependencies in service-graph.ts.
* Extracted to keep service-graph.ts under the 400-line cap.
*/
import type { AppDatabase } from "./database";
import { createSkillService } from "./skill-service";
import type { createSessionManagementService } from "./session-management-service";
import type { RuntimeSkillEntry } from "./skill-types";
interface Logger {
info: (...args: unknown[]) => void;
warn: (...args: unknown[]) => void;
error: (...args: unknown[]) => void;
}
export function makeListRuntimeSkillEntries(deps: { db: AppDatabase; logger: Logger }) {
let cache: ReturnType<typeof createSkillService> | null = null;
return (companyId: string, agentId: string, connectorId: string): RuntimeSkillEntry[] => {
try {
if (!cache) cache = createSkillService({ db: deps.db, profileDir: deps.db.profileDir, logger: deps.logger });
return cache.listRuntimeSkillEntries(companyId, agentId, connectorId);
} catch (error) {
deps.logger.warn(`[service-graph] listRuntimeSkillEntries failed for agent ${agentId}:`, error);
return [];
}
};
}
export function makeSessionHandoffLookup(
deps: { logger: Logger },
getSessionService: () => ReturnType<typeof createSessionManagementService>,
) {
return (taskId: string): string | null => {
try {
return getSessionService().getHandoffContext(taskId);
} catch (error) {
deps.logger.warn(`[service-graph] getSessionHandoff failed for task ${taskId}:`, error);
return null;
}
};
}
export function makePreRunRotationEvaluator(
deps: { db: AppDatabase; logger: Logger },
getSessionService: () => ReturnType<typeof createSessionManagementService>,
) {
return (taskId: string, connectorId: string): { rotated: boolean; handoff: string | null } => {
try {
const sessionSvc = getSessionService();
const activeSession = deps.db.getActiveTaskSession(taskId);
if (!activeSession) return { rotated: false, handoff: null };
const result = sessionSvc.evaluateRotation(activeSession, connectorId);
if (result.shouldRotate) {
sessionSvc.rotateSession(activeSession);
const handoff = sessionSvc.getHandoffContext(taskId);
return { rotated: true, handoff };
}
return { rotated: false, handoff: null };
} catch (error) {
deps.logger.warn(`[service-graph] evaluatePreRunSessionRotation failed for task ${taskId}:`, error);
return { rotated: false, handoff: null };
}
};
}
+58
View File
@@ -0,0 +1,58 @@
/**
* Pi-mono runtime + credential service factory.
*
* Composes `CredentialStore`, `CredentialResolver`, `OAuthLoginService`, and
* `RuntimeBridge` for `service-graph.ts`. Extracted to keep `service-graph.ts`
* under the 400-line cap.
*/
import type { AppDatabase } from "./database";
import type { DesktopEvent } from "@shared/types";
import { CredentialResolver, CredentialStore, OAuthLoginService } from "./credentials";
import { RuntimeBridge } from "./agent-runtime";
export interface PiRuntimeServicesDeps {
db: AppDatabase;
emitEvent: (event: DesktopEvent) => void;
refreshConnectorHealth: (connectorId: string) => Promise<void>;
logger: {
info: (...args: unknown[]) => void;
warn: (...args: unknown[]) => void;
error: (...args: unknown[]) => void;
};
}
export interface PiRuntimeServices {
credentialStore: CredentialStore;
credentialResolver: CredentialResolver;
oauthLoginService: OAuthLoginService;
runtimeBridge: RuntimeBridge;
release: () => void;
}
export function createPiRuntimeServices(deps: PiRuntimeServicesDeps): PiRuntimeServices {
const credentialStore = new CredentialStore({ db: deps.db });
const credentialResolver = new CredentialResolver(credentialStore);
const oauthLoginService = new OAuthLoginService({
store: credentialStore,
emitEvent: deps.emitEvent,
refreshConnectorHealth: deps.refreshConnectorHealth,
logger: deps.logger,
});
const runtimeBridge = new RuntimeBridge({
db: deps.db,
emitEvent: deps.emitEvent,
logger: deps.logger,
credentialResolver,
});
return {
credentialStore,
credentialResolver,
oauthLoginService,
runtimeBridge,
release: () => {
runtimeBridge.approvalHook.cancelAll("AgentCompany shutting down.");
},
};
}
+25
View File
@@ -0,0 +1,25 @@
/**
* Silences the `node:sqlite` ExperimentalWarning emitted on first import.
* Must be imported before any module that touches `node:sqlite`, otherwise
* the warning has already fired.
*
* Targeted: only the SQLite experimental warning. All other warnings pass
* through unchanged.
*/
const originalEmitWarning = process.emitWarning.bind(process);
function isSqliteExperimentalWarning(warning: unknown): boolean {
if (typeof warning === "string") {
return /SQLite is an experimental feature/.test(warning);
}
if (warning instanceof Error) {
return warning.name === "ExperimentalWarning" && /SQLite/.test(warning.message);
}
return false;
}
process.emitWarning = ((warning: unknown, ...rest: unknown[]) => {
if (isSqliteExperimentalWarning(warning)) return;
return (originalEmitWarning as (...args: unknown[]) => void)(warning, ...rest);
}) as typeof process.emitWarning;
+81
View File
@@ -0,0 +1,81 @@
/**
* Defensive token redactor for worker process logs.
*
* Pi-mono's HTTP clients are not known to log credentials, but stack traces
* from upstream libraries (or future pi versions) could include header
* fragments containing `Authorization: Bearer ...` or `?access_token=...`.
* To prevent any such fragment from reaching the parent's stderr stream,
* `installLogRedactor()` wraps `console.error`, `console.warn`, `console.log`,
* `process.stdout.write`, and `process.stderr.write` so that every string
* argument is filtered through `redactTokens()` before transmission.
*/
const PATTERNS: Array<{ pattern: RegExp; replacement: string }> = [
{ pattern: /Bearer\s+[A-Za-z0-9._\-+/=]{8,}/g, replacement: "Bearer [REDACTED]" },
{ pattern: /(authorization\s*[:=]\s*)([^\s,;]{8,})/gi, replacement: "$1[REDACTED]" },
{ pattern: /(access_token=)([^&\s"']+)/gi, replacement: "$1[REDACTED]" },
{ pattern: /(refresh_token=)([^&\s"']+)/gi, replacement: "$1[REDACTED]" },
{ pattern: /(api[_-]?key\s*[:=]\s*['"]?)([A-Za-z0-9._-]{12,})/gi, replacement: "$1[REDACTED]" },
{ pattern: /sk-(?:ant-)?[A-Za-z0-9_-]{16,}/g, replacement: "sk-[REDACTED]" },
{ pattern: /ghp_[A-Za-z0-9]{16,}/g, replacement: "ghp_[REDACTED]" },
{ pattern: /gho_[A-Za-z0-9]{16,}/g, replacement: "gho_[REDACTED]" },
{ pattern: /AKIA[A-Z0-9]{16}/g, replacement: "AKIA[REDACTED]" },
];
export function redactTokens(text: string): string {
let result = text;
for (const { pattern, replacement } of PATTERNS) {
result = result.replace(pattern, replacement);
}
return result;
}
function redactArg(arg: unknown): unknown {
if (typeof arg === "string") return redactTokens(arg);
if (arg instanceof Error) {
const redacted = new Error(redactTokens(arg.message));
redacted.name = arg.name;
if (arg.stack) redacted.stack = redactTokens(arg.stack);
return redacted;
}
return arg;
}
interface InstallTarget {
console: { log: typeof console.log; warn: typeof console.warn; error: typeof console.error; info: typeof console.info };
stdoutWrite: typeof process.stdout.write;
stderrWrite: typeof process.stderr.write;
}
export function installLogRedactor(target?: Partial<InstallTarget>): () => void {
const cons = target?.console ?? console;
const origLog = cons.log.bind(cons);
const origWarn = cons.warn.bind(cons);
const origError = cons.error.bind(cons);
const origInfo = cons.info.bind(cons);
cons.log = (...args: unknown[]) => origLog(...args.map(redactArg));
cons.warn = (...args: unknown[]) => origWarn(...args.map(redactArg));
cons.error = (...args: unknown[]) => origError(...args.map(redactArg));
cons.info = (...args: unknown[]) => origInfo(...args.map(redactArg));
const stdoutWriteOrig = (target?.stdoutWrite ?? process.stdout.write).bind(process.stdout);
const stderrWriteOrig = (target?.stderrWrite ?? process.stderr.write).bind(process.stderr);
process.stdout.write = ((chunk: unknown, ...rest: unknown[]) => {
if (typeof chunk === "string") return stdoutWriteOrig(redactTokens(chunk), ...rest as never[]);
return stdoutWriteOrig(chunk as never, ...rest as never[]);
}) as typeof process.stdout.write;
process.stderr.write = ((chunk: unknown, ...rest: unknown[]) => {
if (typeof chunk === "string") return stderrWriteOrig(redactTokens(chunk), ...rest as never[]);
return stderrWriteOrig(chunk as never, ...rest as never[]);
}) as typeof process.stderr.write;
return () => {
cons.log = origLog;
cons.warn = origWarn;
cons.error = origError;
cons.info = origInfo;
process.stdout.write = stdoutWriteOrig;
process.stderr.write = stderrWriteOrig;
};
}