pikiloom 0.4.36 → 0.4.38
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/agent/drivers/codex.js +20 -3
- package/dist/agent/index.js +2 -2
- package/dist/agent/kernel-bridge.js +206 -0
- package/dist/agent/stream.js +60 -2
- package/dist/cli/kernel-app.js +115 -0
- package/dist/cli/main.js +7 -0
- package/package.json +4 -2
- package/packages/kernel/README.md +107 -0
- package/packages/kernel/dist/contracts/driver.d.ts +95 -0
- package/packages/kernel/dist/contracts/driver.js +1 -0
- package/packages/kernel/dist/contracts/ports.d.ts +84 -0
- package/packages/kernel/dist/contracts/ports.js +1 -0
- package/packages/kernel/dist/contracts/surface.d.ts +75 -0
- package/packages/kernel/dist/contracts/surface.js +1 -0
- package/packages/kernel/dist/drivers/claude.d.ts +23 -0
- package/packages/kernel/dist/drivers/claude.js +453 -0
- package/packages/kernel/dist/drivers/codex.d.ts +14 -0
- package/packages/kernel/dist/drivers/codex.js +346 -0
- package/packages/kernel/dist/drivers/echo.d.ts +20 -0
- package/packages/kernel/dist/drivers/echo.js +61 -0
- package/packages/kernel/dist/drivers/gemini.d.ts +18 -0
- package/packages/kernel/dist/drivers/gemini.js +143 -0
- package/packages/kernel/dist/drivers/hermes.d.ts +14 -0
- package/packages/kernel/dist/drivers/hermes.js +194 -0
- package/packages/kernel/dist/drivers/index.d.ts +5 -0
- package/packages/kernel/dist/drivers/index.js +5 -0
- package/packages/kernel/dist/index.d.ts +18 -0
- package/packages/kernel/dist/index.js +29 -0
- package/packages/kernel/dist/ports/defaults.d.ts +59 -0
- package/packages/kernel/dist/ports/defaults.js +137 -0
- package/packages/kernel/dist/protocol/index.d.ts +309 -0
- package/packages/kernel/dist/protocol/index.js +59 -0
- package/packages/kernel/dist/runtime/hub.d.ts +65 -0
- package/packages/kernel/dist/runtime/hub.js +260 -0
- package/packages/kernel/dist/runtime/loom.d.ts +44 -0
- package/packages/kernel/dist/runtime/loom.js +69 -0
- package/packages/kernel/dist/runtime/pty.d.ts +23 -0
- package/packages/kernel/dist/runtime/pty.js +69 -0
- package/packages/kernel/dist/runtime/session-runner.d.ts +27 -0
- package/packages/kernel/dist/runtime/session-runner.js +210 -0
- package/packages/kernel/dist/runtime/tui.d.ts +8 -0
- package/packages/kernel/dist/runtime/tui.js +35 -0
- package/packages/kernel/dist/runtime/turn.d.ts +17 -0
- package/packages/kernel/dist/runtime/turn.js +16 -0
- package/packages/kernel/dist/surfaces/cli.d.ts +19 -0
- package/packages/kernel/dist/surfaces/cli.js +65 -0
- package/packages/kernel/dist/surfaces/index.d.ts +2 -0
- package/packages/kernel/dist/surfaces/index.js +2 -0
- package/packages/kernel/dist/surfaces/web.d.ts +39 -0
- package/packages/kernel/dist/surfaces/web.js +244 -0
|
@@ -773,6 +773,23 @@ function createCodexStreamState(opts) {
|
|
|
773
773
|
generatingImages: 0,
|
|
774
774
|
};
|
|
775
775
|
}
|
|
776
|
+
// Codex on a ChatGPT-account login rejects non-OpenAI models with a raw upstream JSON blob
|
|
777
|
+
// (e.g. {"detail":"The 'deepseek-v4-pro' model is not supported when using Codex with a
|
|
778
|
+
// ChatGPT account."}). When a third-party model is correctly bound to a Provider profile the
|
|
779
|
+
// injector routes around this; this only fires when no binding exists, so turn the opaque
|
|
780
|
+
// upstream error into an actionable instruction instead of leaking JSON to the chat bubble.
|
|
781
|
+
export function humanizeCodexError(raw) {
|
|
782
|
+
if (!raw)
|
|
783
|
+
return null;
|
|
784
|
+
const match = raw.match(/The '([^']+)' model is not supported when using Codex with a ChatGPT account/i);
|
|
785
|
+
if (match) {
|
|
786
|
+
const model = match[1];
|
|
787
|
+
return `Codex 正在使用 ChatGPT 账号登录,无法运行第三方模型「${model}」。`
|
|
788
|
+
+ `请在「智能体配置」页接入该模型的供应商(填入 Base URL 与 API Key),添加对应模型档案并绑定到 Codex,`
|
|
789
|
+
+ `或改用 Codex 原生模型(如 gpt-5.5)。`;
|
|
790
|
+
}
|
|
791
|
+
return raw;
|
|
792
|
+
}
|
|
776
793
|
function codexErrorResult(error, start, sessionId, model, thinkingEffort) {
|
|
777
794
|
return {
|
|
778
795
|
ok: false, message: error, thinking: null,
|
|
@@ -1119,7 +1136,7 @@ export async function doCodexStream(opts) {
|
|
|
1119
1136
|
if (threadResp.error) {
|
|
1120
1137
|
const errMsg = threadResp.error.message || 'thread/start failed';
|
|
1121
1138
|
agentWarn(`[codex-rpc] thread error: ${errMsg}`);
|
|
1122
|
-
return codexErrorResult(errMsg, start, opts.sessionId, opts.model, opts.thinkingEffort);
|
|
1139
|
+
return codexErrorResult(humanizeCodexError(errMsg) ?? errMsg, start, opts.sessionId, opts.model, opts.thinkingEffort);
|
|
1123
1140
|
}
|
|
1124
1141
|
const threadResult = threadResp.result;
|
|
1125
1142
|
s.sessionId = threadResult.thread?.id ?? s.sessionId;
|
|
@@ -1210,7 +1227,7 @@ export async function doCodexStream(opts) {
|
|
|
1210
1227
|
unsubscribeRequests();
|
|
1211
1228
|
const errMsg = turnResp.error.message || 'turn/start failed';
|
|
1212
1229
|
agentWarn(`[codex-rpc] turn/start error: ${errMsg}`);
|
|
1213
|
-
return codexErrorResult(errMsg, start, s.sessionId, s.model, s.thinkingEffort);
|
|
1230
|
+
return codexErrorResult(humanizeCodexError(errMsg) ?? errMsg, start, s.sessionId, s.model, s.thinkingEffort);
|
|
1214
1231
|
}
|
|
1215
1232
|
s.turnId = turnResp.result?.turn?.id ?? null;
|
|
1216
1233
|
publishTurnControl();
|
|
@@ -1226,7 +1243,7 @@ export async function doCodexStream(opts) {
|
|
|
1226
1243
|
tryEmitCodexImageBlock(s, callId);
|
|
1227
1244
|
}
|
|
1228
1245
|
const ok = s.turnStatus === 'completed' && !timedOut && !interrupted;
|
|
1229
|
-
const error = s.turnError
|
|
1246
|
+
const error = humanizeCodexError(s.turnError)
|
|
1230
1247
|
|| (interrupted ? 'Interrupted by user.' : null)
|
|
1231
1248
|
|| (timedOut ? `Timed out after ${opts.timeout}s waiting for turn completion.` : null)
|
|
1232
1249
|
|| (!ok ? `Turn ${s.turnStatus || 'unknown'}.` : null);
|
package/dist/agent/index.js
CHANGED
|
@@ -7,7 +7,7 @@ export { attachAgentImage, attachInlineImage, materializeImage, rewriteAttachmen
|
|
|
7
7
|
export { deliverArtifact, readDeliveredArtifacts, deliveredArtifactBlocks, tailDeliveredBlocks, latestDeliveredTaskId, mimeForArtifact, } from './artifacts.js';
|
|
8
8
|
export { Q, agentLog, agentWarn, agentError, dedupeStrings, numberOrNull, normalizeStreamPreviewPlan, parseTodoWriteAsPlan, normalizeActivityLine, pushRecentActivity, detectClaudeApiError, isRetryableClaudeApiError, detectClaudeModelError, claudeModelErrorMessage, firstNonEmptyLine, shortValue, normalizeErrorMessage, joinErrorMessages, appendSystemPrompt, mimeForExt, computeContext, buildStreamPreviewMeta, summarizeClaudeToolUse, summarizeClaudeToolResult, previewToolCallInput, previewToolCallResult, roundPercent, toIsoFromEpochSeconds, normalizeUsageStatus, labelFromWindowMinutes, usageWindowFromRateLimit, parseJsonTail, modelFamily, normalizeClaudeModelId, emptyUsage, readTailLines, stripInjectedPrompts, sanitizeSessionUserPreviewText, SESSION_PREVIEW_IMAGE_PLACEHOLDER_RE, CLAUDE_AT_MENTION_IMAGE_RE, extractClaudeAtMentionImagePaths, stripClaudeAtMentionImages, isPendingSessionId, emitSessionIdUpdate, sessionListDisplayTitle, } from './utils.js';
|
|
9
9
|
export { updateSessionMeta, promoteSessionId, recordFork, resolveCanonicalSessionId, getSessionPromotions, listPikiloomSessions, findPikiloomSession, getSessionStoredConfig, ensureManagedSession, findManagedThreadSession, stageSessionFiles, mergeManagedAndNativeSessions, managedRecordToSessionInfo, getSessions, getSessionTail, getSessionMessages, applyTurnWindow, applyTurnFilter, classifySession, deriveUserStatus, exportSession, importSession, deleteAgentSession, isProcessAlive, isRunningSessionStale, reconcileOrphanedRunningSessions, } from './session.js';
|
|
10
|
-
export { detectAgentBin, listAgents, resolveDefaultAgent, run, doStream, listModels, resolveAgentModels, getUsage, getAgentBoundModelId, setAgentBoundModelId, } from './stream.js';
|
|
10
|
+
export { detectAgentBin, listAgents, resolveDefaultAgent, run, doStream, recoverProfileIdForModel, listModels, resolveAgentModels, getUsage, getAgentBoundModelId, setAgentBoundModelId, } from './stream.js';
|
|
11
11
|
export { registerDriver, getDriver, getDriverCapabilities, allDrivers, allDriverIds, hasDriver, shutdownAllDrivers, } from './driver.js';
|
|
12
12
|
export { getProjectSkillPaths, initializeProjectSkills, listSkills, getGlobalSkillsRoot, collapseSkillPrompt, } from './skills.js';
|
|
13
13
|
export { readGoal, writeGoal, clearGoal, setGoal, pauseGoal, resumeGoal, completeGoal, accountTurn, bumpContinuationCount, shouldContinueAfterTurn, renderContinuationPrompt, renderBudgetLimitPrompt, sessionGoalPath, DEFAULT_MAX_CONTINUATIONS, } from './goal.js';
|
|
@@ -19,6 +19,6 @@ export { getMcpToken, saveMcpToken, deleteMcpToken, hasValidMcpToken, startAutho
|
|
|
19
19
|
export { installSkill, removeSkill, checkSkillUpdates, getGlobalSkillsDir, recordSkillInstall, getSkillLedgerEntry, forgetSkillInstall, normalizeSkillSourceKey, } from './skill-installer.js';
|
|
20
20
|
export { getRecommendedClis, getRecommendedCli, detectCli, getCachedCliStatus, invalidateCliStatus, currentPlatform, getCliCatalog, refreshCliStatus, startCliAuthSession, getAuthSession, cancelAuthSession, applyCliToken, logoutCli, startCliInstallSession, } from './cli/index.js';
|
|
21
21
|
export { doClaudeStream } from './drivers/claude.js';
|
|
22
|
-
export { doCodexStream, buildCodexTurnInput, shutdownCodexServer, getCodexUsageLive } from './drivers/codex.js';
|
|
22
|
+
export { doCodexStream, buildCodexTurnInput, shutdownCodexServer, getCodexUsageLive, humanizeCodexError } from './drivers/codex.js';
|
|
23
23
|
export { doGeminiStream } from './drivers/gemini.js';
|
|
24
24
|
export { doHermesStream } from './drivers/hermes.js';
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
+
import { agentLog, agentWarn } from './utils.js';
|
|
6
|
+
import { normalizeClaudeSessionEntrypoint } from './drivers/claude.js';
|
|
7
|
+
import { humanizeCodexError } from './drivers/codex.js';
|
|
8
|
+
// ── The cutover seam: route an agent turn through @pikiloom/kernel ──────────────
|
|
9
|
+
//
|
|
10
|
+
// DEFAULT: ON — claude/codex/gemini/hermes turns run on the kernel drivers (via runTurn).
|
|
11
|
+
// Escape hatches to the legacy driver path:
|
|
12
|
+
// LOOM_KERNEL_PIPELINE=0 (env; force legacy at startup; survives dev.sh scrub)
|
|
13
|
+
// ~/.pikiloom/dev/kernel-legacy.on (file; hot-toggle legacy without restart)
|
|
14
|
+
// LOOM_KERNEL_PIPELINE=1 (env; force kernel, overriding the file)
|
|
15
|
+
// Tests always run legacy (the unit suite asserts legacy driver behavior). The bridge
|
|
16
|
+
// re-applies app-level parity the pure kernel must not own (claude jsonl entrypoint, codex humanize).
|
|
17
|
+
const KERNEL_AGENTS = new Set(['claude', 'codex', 'gemini', 'hermes']);
|
|
18
|
+
export function shouldUseKernelPipeline(agent) {
|
|
19
|
+
if (!KERNEL_AGENTS.has(agent))
|
|
20
|
+
return false;
|
|
21
|
+
if (process.env.VITEST || process.env.NODE_ENV === 'test')
|
|
22
|
+
return false; // tests assert legacy
|
|
23
|
+
if (process.env.LOOM_KERNEL_PIPELINE === '0')
|
|
24
|
+
return false; // explicit legacy
|
|
25
|
+
if (process.env.LOOM_KERNEL_PIPELINE === '1')
|
|
26
|
+
return true; // explicit kernel
|
|
27
|
+
try {
|
|
28
|
+
if (fs.existsSync(path.join(os.homedir(), '.pikiloom', 'dev', 'kernel-legacy.on')))
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
catch { /* ignore */ }
|
|
32
|
+
return true; // default: kernel
|
|
33
|
+
}
|
|
34
|
+
// Build the kernel driver instance + the per-agent AgentTurnInput from pikiloom StreamOpts.
|
|
35
|
+
function buildKernelDriver(kernel, opts) {
|
|
36
|
+
const common = {
|
|
37
|
+
prompt: opts.prompt,
|
|
38
|
+
workdir: opts.workdir,
|
|
39
|
+
sessionId: opts.sessionId ?? null,
|
|
40
|
+
attachments: opts.attachments,
|
|
41
|
+
effort: opts.thinkingEffort,
|
|
42
|
+
env: opts.extraEnv,
|
|
43
|
+
mcpConfigPath: opts.mcpConfigPath ?? null,
|
|
44
|
+
};
|
|
45
|
+
switch (opts.agent) {
|
|
46
|
+
case 'codex': {
|
|
47
|
+
// codexExtraArgs is a flattened ['-c','k=v','-c','k=v',...]; extract the k=v values
|
|
48
|
+
// so the kernel keeps BYOK provider routing.
|
|
49
|
+
const ce = opts.codexExtraArgs || [];
|
|
50
|
+
const configOverrides = [];
|
|
51
|
+
for (let i = 0; i < ce.length; i++)
|
|
52
|
+
if (ce[i] === '-c' && ce[i + 1])
|
|
53
|
+
configOverrides.push(ce[++i]);
|
|
54
|
+
return { driver: new kernel.CodexDriver(), input: { ...common, model: opts.codexModel ?? opts.model ?? null, systemPrompt: opts.codexDeveloperInstructions, configOverrides, fullAccess: opts.codexFullAccess } };
|
|
55
|
+
}
|
|
56
|
+
case 'gemini':
|
|
57
|
+
return { driver: new kernel.GeminiDriver(), input: { ...common, model: opts.geminiModel ?? opts.model ?? null, systemPrompt: opts.geminiSystemInstruction, extraArgs: opts.geminiExtraArgs } };
|
|
58
|
+
case 'hermes':
|
|
59
|
+
return { driver: new kernel.HermesDriver(), input: { ...common, model: opts.hermesModel ?? opts.model ?? null } };
|
|
60
|
+
case 'claude':
|
|
61
|
+
default:
|
|
62
|
+
return {
|
|
63
|
+
driver: new kernel.ClaudeDriver(),
|
|
64
|
+
input: { ...common, model: opts.claudeModel ?? opts.model ?? null, systemPrompt: opts.claudeAppendSystemPrompt, permissionMode: opts.claudePermissionMode ?? null, extraArgs: opts.claudeExtraArgs, steerable: !!opts.onSteerReady },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
let _kernel = null;
|
|
69
|
+
export async function loadKernel() {
|
|
70
|
+
if (_kernel)
|
|
71
|
+
return _kernel;
|
|
72
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
73
|
+
const candidates = [
|
|
74
|
+
path.resolve(here, '../../packages/kernel/dist/index.js'), // src/agent (dev) & dist/agent (prod) -> <repo>/packages/kernel
|
|
75
|
+
path.resolve(process.cwd(), 'packages/kernel/dist/index.js'),
|
|
76
|
+
];
|
|
77
|
+
// Prefer an installed package if present, else the in-repo built dist.
|
|
78
|
+
// (specifier kept in a variable so tsc treats it as a dynamic any, not a static resolve)
|
|
79
|
+
const pkgName = '@pikiloom/kernel';
|
|
80
|
+
try {
|
|
81
|
+
_kernel = await import(pkgName);
|
|
82
|
+
return _kernel;
|
|
83
|
+
}
|
|
84
|
+
catch { /* not installed */ }
|
|
85
|
+
for (const c of candidates) {
|
|
86
|
+
if (fs.existsSync(c)) {
|
|
87
|
+
_kernel = await import(pathToFileURL(c).href);
|
|
88
|
+
return _kernel;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
throw new Error(`@pikiloom/kernel not found (build it: npm run build in packages/kernel). Looked in: ${candidates.join(', ')}`);
|
|
92
|
+
}
|
|
93
|
+
// A kernel 'artifact' (e.g. a generated image as a data URL) -> write to a temp file and
|
|
94
|
+
// deliver through pikiloom's normal file-delivery seam (same path as im_send_file).
|
|
95
|
+
async function deliverKernelArtifact(artifact, opts) {
|
|
96
|
+
try {
|
|
97
|
+
const send = opts.mcpSendFile;
|
|
98
|
+
if (typeof send !== 'function' || !artifact?.url)
|
|
99
|
+
return;
|
|
100
|
+
const m = String(artifact.url).match(/^data:([^;]+);base64,(.*)$/s);
|
|
101
|
+
if (!m)
|
|
102
|
+
return;
|
|
103
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'kernel-art-'));
|
|
104
|
+
const file = path.join(dir, artifact.fileName || 'artifact.bin');
|
|
105
|
+
fs.writeFileSync(file, Buffer.from(m[2], 'base64'));
|
|
106
|
+
await send(file, { kind: artifact.kind || 'document', caption: artifact.caption });
|
|
107
|
+
}
|
|
108
|
+
catch { /* best-effort delivery */ }
|
|
109
|
+
}
|
|
110
|
+
export async function kernelStream(opts) {
|
|
111
|
+
const start = Date.now();
|
|
112
|
+
const kernel = await loadKernel();
|
|
113
|
+
const { driver, input } = buildKernelDriver(kernel, opts);
|
|
114
|
+
agentLog(`[kernel-bridge] routing ${opts.agent} turn via @pikiloom/kernel ${driver?.constructor?.name || 'Driver'}`);
|
|
115
|
+
let delivered = 0; // artifacts already sent out-of-band
|
|
116
|
+
let seenSid = null;
|
|
117
|
+
// SessionRunner (via runTurn) owns the event accumulation; the bridge only translates
|
|
118
|
+
// the kernel's UniversalSnapshot onto pikiloom's preview (onText) and delivers any new
|
|
119
|
+
// artifacts through the normal file seam. This is the "product bridge" pattern: map your
|
|
120
|
+
// request -> AgentTurnInput, map snapshot -> your UI, map result -> your result shape.
|
|
121
|
+
const onSnapshot = (s) => {
|
|
122
|
+
const arts = s.artifacts || [];
|
|
123
|
+
for (; delivered < arts.length; delivered++)
|
|
124
|
+
void deliverKernelArtifact(arts[delivered], opts);
|
|
125
|
+
if (s.sessionId && s.sessionId !== seenSid) {
|
|
126
|
+
seenSid = s.sessionId;
|
|
127
|
+
try {
|
|
128
|
+
opts.onSessionId?.(s.sessionId);
|
|
129
|
+
}
|
|
130
|
+
catch { /* ignore */ }
|
|
131
|
+
}
|
|
132
|
+
const m = {
|
|
133
|
+
inputTokens: s.usage?.inputTokens ?? null,
|
|
134
|
+
outputTokens: s.usage?.outputTokens ?? null,
|
|
135
|
+
cachedInputTokens: s.usage?.cachedInputTokens ?? null,
|
|
136
|
+
contextUsedTokens: s.usage?.contextUsedTokens ?? null,
|
|
137
|
+
contextPercent: s.usage?.contextPercent ?? null,
|
|
138
|
+
toolCalls: s.toolCalls?.length ? s.toolCalls : undefined,
|
|
139
|
+
subAgents: s.subAgents?.length ? s.subAgents : undefined,
|
|
140
|
+
providerName: opts.byokProviderName ?? null,
|
|
141
|
+
};
|
|
142
|
+
try {
|
|
143
|
+
opts.onText(s.text || '', s.reasoning || '', s.activity || '', m, s.plan ?? null);
|
|
144
|
+
}
|
|
145
|
+
catch { /* isolate */ }
|
|
146
|
+
};
|
|
147
|
+
let result;
|
|
148
|
+
let snapshot = {};
|
|
149
|
+
try {
|
|
150
|
+
({ result, snapshot } = await kernel.runTurn(driver, input, {
|
|
151
|
+
onSnapshot,
|
|
152
|
+
onSteer: (fn) => { try {
|
|
153
|
+
opts.onSteerReady?.(fn);
|
|
154
|
+
}
|
|
155
|
+
catch { /* ignore */ } },
|
|
156
|
+
signal: opts.abortSignal,
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
agentWarn(`[kernel-bridge] kernel run failed: ${err?.message || err}`);
|
|
161
|
+
result = { ok: false, text: '', error: err?.message || String(err), stopReason: 'error', sessionId: opts.sessionId ?? null };
|
|
162
|
+
}
|
|
163
|
+
const finalSessionId = result.sessionId || snapshot.sessionId || opts.sessionId || null;
|
|
164
|
+
if (finalSessionId && finalSessionId !== seenSid) {
|
|
165
|
+
try {
|
|
166
|
+
opts.onSessionId?.(finalSessionId);
|
|
167
|
+
}
|
|
168
|
+
catch { /* ignore */ }
|
|
169
|
+
}
|
|
170
|
+
// App-level parity the pure kernel must not own (mirrors legacy *.doStream):
|
|
171
|
+
// - claude: rewrite the native jsonl entrypoint sdk-cli->cli (VSCode session visibility).
|
|
172
|
+
// - codex: translate raw provider error JSON (ChatGPT-account / third-party) to prose.
|
|
173
|
+
if (opts.agent === 'claude') {
|
|
174
|
+
try {
|
|
175
|
+
normalizeClaudeSessionEntrypoint(opts.workdir, finalSessionId);
|
|
176
|
+
}
|
|
177
|
+
catch { /* best-effort */ }
|
|
178
|
+
}
|
|
179
|
+
const finalError = (opts.agent === 'codex' && result.error)
|
|
180
|
+
? (humanizeCodexError(result.error) ?? result.error)
|
|
181
|
+
: (result.error ?? null);
|
|
182
|
+
const u = result.usage || snapshot.usage || {};
|
|
183
|
+
return {
|
|
184
|
+
ok: !!result.ok,
|
|
185
|
+
message: (result.text || snapshot.text || '').trim() || (finalError ?? '(no output)'),
|
|
186
|
+
thinking: (result.reasoning || snapshot.reasoning || '').trim() || null,
|
|
187
|
+
plan: snapshot.plan ?? null,
|
|
188
|
+
sessionId: finalSessionId,
|
|
189
|
+
workspacePath: null,
|
|
190
|
+
model: input.model ?? null,
|
|
191
|
+
thinkingEffort: opts.thinkingEffort,
|
|
192
|
+
elapsedS: (Date.now() - start) / 1000,
|
|
193
|
+
inputTokens: u.inputTokens ?? null,
|
|
194
|
+
outputTokens: u.outputTokens ?? null,
|
|
195
|
+
cachedInputTokens: u.cachedInputTokens ?? null,
|
|
196
|
+
cacheCreationInputTokens: null,
|
|
197
|
+
contextWindow: null,
|
|
198
|
+
contextUsedTokens: u.contextUsedTokens ?? null,
|
|
199
|
+
contextPercent: null,
|
|
200
|
+
codexCumulative: null,
|
|
201
|
+
error: finalError,
|
|
202
|
+
stopReason: result.stopReason ?? null,
|
|
203
|
+
incomplete: !result.ok,
|
|
204
|
+
activity: snapshot.activity || null,
|
|
205
|
+
};
|
|
206
|
+
}
|
package/dist/agent/stream.js
CHANGED
|
@@ -12,6 +12,7 @@ import { Q, agentLog, agentWarn, agentError, joinErrorMessages, normalizeErrorMe
|
|
|
12
12
|
import { saveSessionRecord, setSessionRunState, applySessionRunResult, ensureSessionWorkspace, importFilesIntoWorkspace, syncManagedSessionIdentity, summarizePromptTitle, recordFork, resolveCanonicalSessionId, } from './session.js';
|
|
13
13
|
import { clearAwaitResume } from './await-resume.js';
|
|
14
14
|
import { collapseSkillPrompt } from './skills.js';
|
|
15
|
+
import { shouldUseKernelPipeline, kernelStream } from './kernel-bridge.js';
|
|
15
16
|
function trimSessionText(value, max = 24_000) {
|
|
16
17
|
const text = typeof value === 'string' ? value.trim() : '';
|
|
17
18
|
if (!text)
|
|
@@ -388,6 +389,35 @@ function finalizeStreamResult(result, workdir, prompt, session, workflowEnabled,
|
|
|
388
389
|
saveSessionRecord(workdir, session.record);
|
|
389
390
|
return { ...result, sessionId: session.sessionId, workspacePath: session.workspacePath };
|
|
390
391
|
}
|
|
392
|
+
function requestedModelForAgent(opts) {
|
|
393
|
+
switch (opts.agent) {
|
|
394
|
+
case 'claude': return (opts.claudeModel || opts.model || '').trim();
|
|
395
|
+
case 'codex': return (opts.codexModel || opts.model || '').trim();
|
|
396
|
+
case 'gemini': return (opts.geminiModel || opts.model || '').trim();
|
|
397
|
+
case 'hermes': return (opts.hermesModel || opts.model || '').trim();
|
|
398
|
+
}
|
|
399
|
+
return (opts.model || '').trim();
|
|
400
|
+
}
|
|
401
|
+
// When a session carries a third-party model id but its profile binding was lost
|
|
402
|
+
// (stale/deleted profileId, or a model selected without a binding), resolveAgentInjection
|
|
403
|
+
// returns null and the agent silently falls back to its native account — e.g. Codex on a
|
|
404
|
+
// ChatGPT login rejects "deepseek-v4-pro". If exactly one configured profile (for a provider
|
|
405
|
+
// kind this agent accepts) declares that model id, the intent is unambiguous: recover it.
|
|
406
|
+
export function recoverProfileIdForModel(agent, modelId) {
|
|
407
|
+
const model = modelId.trim();
|
|
408
|
+
if (!model)
|
|
409
|
+
return null;
|
|
410
|
+
const accepted = new Set(getAcceptedProviderKinds(agent));
|
|
411
|
+
if (accepted.size === 0)
|
|
412
|
+
return null;
|
|
413
|
+
const matches = listProfiles().filter(profile => {
|
|
414
|
+
if (profile.modelId !== model)
|
|
415
|
+
return false;
|
|
416
|
+
const provider = getProvider(profile.providerId);
|
|
417
|
+
return !!provider && accepted.has(provider.kind);
|
|
418
|
+
});
|
|
419
|
+
return matches.length === 1 ? matches[0].id : null;
|
|
420
|
+
}
|
|
391
421
|
export async function doStream(opts) {
|
|
392
422
|
let session;
|
|
393
423
|
let prepared;
|
|
@@ -446,7 +476,18 @@ export async function doStream(opts) {
|
|
|
446
476
|
agentWarn(`[mcp] bridge start failed: ${e.message} — proceeding without MCP`);
|
|
447
477
|
}
|
|
448
478
|
try {
|
|
449
|
-
|
|
479
|
+
let injection = await resolveAgentInjection(prepared.agent, prepared.profileId);
|
|
480
|
+
if (!injection) {
|
|
481
|
+
const requestedModel = requestedModelForAgent(prepared);
|
|
482
|
+
const recoveredProfileId = recoverProfileIdForModel(prepared.agent, requestedModel);
|
|
483
|
+
if (recoveredProfileId) {
|
|
484
|
+
injection = await resolveAgentInjection(prepared.agent, recoveredProfileId);
|
|
485
|
+
if (injection) {
|
|
486
|
+
prepared.profileId = recoveredProfileId;
|
|
487
|
+
agentLog(`[byok] recovered lost provider binding: agent=${prepared.agent} model=${requestedModel} → profile=${recoveredProfileId}`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
450
491
|
if (injection) {
|
|
451
492
|
prepared.extraEnv = { ...(prepared.extraEnv || {}), ...injection.env };
|
|
452
493
|
if (injection.modelOverride) {
|
|
@@ -518,7 +559,24 @@ export async function doStream(opts) {
|
|
|
518
559
|
throw new Error(`Agent ${prepared.agent} does not support fork`);
|
|
519
560
|
}
|
|
520
561
|
await awaitAgentUpdateIdle(prepared.agent, AGENT_UPDATE_TIMEOUTS.spawnWait);
|
|
521
|
-
|
|
562
|
+
// Cutover seam: when enabled (default ON for claude/codex/gemini/hermes), route the
|
|
563
|
+
// agent turn through @pikiloom/kernel. kernelStream throws ONLY if the kernel package
|
|
564
|
+
// can't be loaded (missing dist) — every in-turn error is returned as a result. So a
|
|
565
|
+
// throw here means "kernel unavailable": fall back to the legacy driver rather than
|
|
566
|
+
// failing the turn. See agent/kernel-bridge.ts.
|
|
567
|
+
let result;
|
|
568
|
+
if (shouldUseKernelPipeline(prepared.agent)) {
|
|
569
|
+
try {
|
|
570
|
+
result = await kernelStream(prepared);
|
|
571
|
+
}
|
|
572
|
+
catch (kernelErr) {
|
|
573
|
+
agentWarn(`[kernel-bridge] kernel unavailable, falling back to legacy driver: ${kernelErr?.message || kernelErr}`);
|
|
574
|
+
result = await driver.doStream(prepared);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
result = await driver.doStream(prepared);
|
|
579
|
+
}
|
|
522
580
|
const finalized = finalizeStreamResult(result, opts.workdir, opts.prompt, session, opts.claudeWorkflowEnabled, opts.profileId);
|
|
523
581
|
finalized.byokProviderName = prepared.byokProviderName ?? null;
|
|
524
582
|
finalized.byokProfileName = prepared.byokProfileName ?? null;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { loadKernel } from '../agent/kernel-bridge.js';
|
|
6
|
+
import { loadUserConfig, applyUserConfig } from '../core/config/user-config.js';
|
|
7
|
+
import { resolveAgentInjection } from '../model/injector.js';
|
|
8
|
+
// ── The "new version": pikiloom's backend booted ON @pikiloom/kernel ────────────
|
|
9
|
+
//
|
|
10
|
+
// Gated by LOOM_KERNEL_APP=1 (non-PIKILOOM_ prefix so it survives dev.sh's env scrub).
|
|
11
|
+
// LOOM_KERNEL_APP=1 npm run dev -> this kernel runtime on the dashboard port
|
|
12
|
+
// npm run dev -> the legacy app (old version), untouched
|
|
13
|
+
//
|
|
14
|
+
// One createLoom() drives every agent through the kernel's Driver registry and exposes it
|
|
15
|
+
// over a WebSurface carrying both lanes on one ws host: Lane S (web/structured snapshot) and
|
|
16
|
+
// Lane R (raw PTY / terminal). The whole C1-C5 SDK surface (history, catalog, HITL, per-session
|
|
17
|
+
// queue, wire) is live here.
|
|
18
|
+
const DEMO_MODELS = {
|
|
19
|
+
claude: [{ id: 'claude-opus-4-8', label: 'Opus 4.8', providerName: 'anthropic', contextWindow: 200000 }],
|
|
20
|
+
codex: [{ id: 'gpt-5.5-codex', label: 'GPT-5.5 Codex', providerName: 'openai', contextWindow: 400000 }],
|
|
21
|
+
gemini: [{ id: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro', providerName: 'google', contextWindow: 1000000 }],
|
|
22
|
+
echo: [{ id: 'echo-1', label: 'Echo (hermetic)', providerName: 'local', contextWindow: 8192 }],
|
|
23
|
+
};
|
|
24
|
+
export async function runKernelApp(argv) {
|
|
25
|
+
const i = argv.indexOf('--dashboard-port');
|
|
26
|
+
const port = (i >= 0 && parseInt(argv[i + 1], 10)) || 3940;
|
|
27
|
+
const k = await loadKernel();
|
|
28
|
+
const { createLoom, ClaudeDriver, CodexDriver, GeminiDriver, HermesDriver, EchoDriver, WebSurface } = k;
|
|
29
|
+
// Load providers/profiles so the active BYOK/豆包 bindings are visible to the resolver.
|
|
30
|
+
try {
|
|
31
|
+
applyUserConfig(loadUserConfig(), undefined, { overwrite: true, clearMissing: true });
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
console.log(`[kernel-app] config load: ${e?.message || e}`);
|
|
35
|
+
}
|
|
36
|
+
// REAL ModelResolver: delegate to pikiloom's resolveAgentInjection (BYOK / 豆包 / native).
|
|
37
|
+
// null => native CLI login; else map the InjectedSpawnConfig onto the kernel's ModelInjection.
|
|
38
|
+
const modelResolver = {
|
|
39
|
+
async resolve(agent, opts) {
|
|
40
|
+
let inj = null;
|
|
41
|
+
try {
|
|
42
|
+
inj = await resolveAgentInjection(agent, opts.profileId ?? undefined);
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
console.log(`[kernel-app] inject ${agent} failed: ${e?.message || e}`);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (!inj)
|
|
49
|
+
return null; // no active profile → native login
|
|
50
|
+
const env = { ...(inj.env || {}) };
|
|
51
|
+
if (inj.homeOverride)
|
|
52
|
+
env.HOME = inj.homeOverride;
|
|
53
|
+
if (inj.configFiles && Object.keys(inj.configFiles).length)
|
|
54
|
+
console.log(`[kernel-app] note: ${agent} injection has configFiles (not yet threaded through kernel)`);
|
|
55
|
+
return {
|
|
56
|
+
model: inj.modelOverride ?? opts.model ?? null,
|
|
57
|
+
env,
|
|
58
|
+
extraArgs: inj.argvAppend?.length ? inj.argvAppend : undefined,
|
|
59
|
+
configOverrides: inj.codexConfigOverrides?.length ? inj.codexConfigOverrides : undefined,
|
|
60
|
+
providerName: inj.providerName ?? null,
|
|
61
|
+
contextWindow: inj.contextWindow ?? null,
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
// Discovery (C2). A full cutover wires pikiloom's runtime-config / catalog / mcp SSOT
|
|
66
|
+
// through this port; here a representative catalog proves discovery end to end.
|
|
67
|
+
const catalog = {
|
|
68
|
+
async listModels({ agent }) { return DEMO_MODELS[agent] || []; },
|
|
69
|
+
async listEffort() { return [{ id: 'low' }, { id: 'medium' }, { id: 'high' }]; },
|
|
70
|
+
async listTools() { return []; },
|
|
71
|
+
async listSkills() { return []; },
|
|
72
|
+
};
|
|
73
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
74
|
+
let html = '<!doctype html><meta charset=utf-8><title>pikiloom · kernel</title>'
|
|
75
|
+
+ '<body style="font:14px system-ui;padding:2rem;max-width:42rem">'
|
|
76
|
+
+ '<h1>pikiloom — kernel runtime (new version)</h1>'
|
|
77
|
+
+ '<p>The WebSurface ws host is live on this port. Connect any pikichannel/ws client and speak the UniversalSnapshot wire protocol (hello → subscribe → prompt; getHistory / getCatalog).</p>';
|
|
78
|
+
for (const c of [
|
|
79
|
+
path.resolve(here, '../../packages/kernel/examples/console.html'),
|
|
80
|
+
path.resolve(process.cwd(), 'packages/kernel/examples/console.html'),
|
|
81
|
+
]) {
|
|
82
|
+
try {
|
|
83
|
+
html = fs.readFileSync(c, 'utf8');
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
catch { /* fallback inline */ }
|
|
87
|
+
}
|
|
88
|
+
const server = http.createServer((req, res) => {
|
|
89
|
+
if (req.method === 'GET' && (req.url === '/' || req.url?.startsWith('/?'))) {
|
|
90
|
+
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-cache' });
|
|
91
|
+
res.end(html);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
res.writeHead(404);
|
|
95
|
+
res.end('not found');
|
|
96
|
+
});
|
|
97
|
+
// WebSurface serves BOTH lanes: Lane S (Web/structured snapshot) and Lane R (Surface/
|
|
98
|
+
// raw PTY, via the TuiHost the kernel hands it at start). One ws host, two lanes.
|
|
99
|
+
const web = new WebSurface({ server, name: 'pikiloom-kernel' });
|
|
100
|
+
const surfaces = [web];
|
|
101
|
+
const loom = createLoom({
|
|
102
|
+
appNamespace: 'pikiloom-kernel',
|
|
103
|
+
drivers: [new EchoDriver(), new ClaudeDriver(), new CodexDriver(), new GeminiDriver(), new HermesDriver()],
|
|
104
|
+
defaultAgent: 'claude',
|
|
105
|
+
surfaces,
|
|
106
|
+
catalog,
|
|
107
|
+
modelResolver,
|
|
108
|
+
log: (m) => console.log(`[kernel-app] ${m}`),
|
|
109
|
+
});
|
|
110
|
+
await loom.start();
|
|
111
|
+
await new Promise((resolve) => server.listen(port, () => resolve()));
|
|
112
|
+
const st = loom.status();
|
|
113
|
+
console.log(`[kernel-app] NEW VERSION up — http+ws http://localhost:${port} agents=${st.agents.join(',')} surfaces=${st.surfaces.join(',')}`);
|
|
114
|
+
// The http+ws server keeps the process alive.
|
|
115
|
+
}
|
package/dist/cli/main.js
CHANGED
|
@@ -590,6 +590,13 @@ async function launchChannels(channels, dashboard) {
|
|
|
590
590
|
export async function main() {
|
|
591
591
|
if (await handleMcpServeMode())
|
|
592
592
|
return;
|
|
593
|
+
// Cutover gate: LOOM_KERNEL_APP=1 boots the backend on @pikiloom/kernel (new version)
|
|
594
|
+
// instead of the legacy app. Non-PIKILOOM_ prefix so it survives dev.sh's env scrub.
|
|
595
|
+
if (process.env.LOOM_KERNEL_APP === '1') {
|
|
596
|
+
const { runKernelApp } = await import('./kernel-app.js');
|
|
597
|
+
await runKernelApp(process.argv.slice(2));
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
593
600
|
const args = parseArgs(process.argv.slice(2));
|
|
594
601
|
let userConfig = loadUserConfig();
|
|
595
602
|
if (args.version) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pikiloom",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.38",
|
|
4
4
|
"description": "Put the world's smartest AI agents in your pocket. Command local Claude & Gemini via IM. | 让最好用的 IM 变成你电脑上的顶级 Agent 控制台",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"dist/",
|
|
11
11
|
"dashboard/dist/",
|
|
12
|
+
"packages/kernel/dist/",
|
|
12
13
|
"LICENSE",
|
|
13
14
|
"README.md"
|
|
14
15
|
],
|
|
@@ -33,7 +34,8 @@
|
|
|
33
34
|
"build:dashboard": "vite build --config dashboard/vite.config.ts",
|
|
34
35
|
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
35
36
|
"copy:pikichannel-web": "node -e \"require('fs').cpSync('src/pikichannel/web','dist/pikichannel/web',{recursive:true})\"",
|
|
36
|
-
"build": "
|
|
37
|
+
"build:kernel": "tsc -p packages/kernel/tsconfig.json",
|
|
38
|
+
"build": "npm run clean && npm run build:dashboard && tsc && npm run build:kernel && npm run copy:pikichannel-web",
|
|
37
39
|
"postbuild": "node -e \"require('fs').chmodSync('dist/cli/main.js',0o755)\"",
|
|
38
40
|
"prepack": "npm run build",
|
|
39
41
|
"test": "vitest run",
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# @pikiloom/kernel
|
|
2
|
+
|
|
3
|
+
Heterogeneous coding agents (Claude / Codex / Gemini / ACP) → an **interaction-friendly,
|
|
4
|
+
accumulating session snapshot + control handle**, exposed over **pluggable surfaces**
|
|
5
|
+
(IM, Web, tunnel). Environmental concerns are **injected ports** with working defaults.
|
|
6
|
+
|
|
7
|
+
This is the reusable core **pikiloom itself is meant to be built on**. Drop it into any
|
|
8
|
+
project and stand up a "pikiloom-like" backend in a few lines.
|
|
9
|
+
|
|
10
|
+
## The model (one package, three rings)
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
上层 (you write): IM bindings · Web UI/skin · Plugins ── implement Surface / Plugin
|
|
14
|
+
▲ createLoom({ surfaces, plugins, ...ports })
|
|
15
|
+
@pikiloom/kernel: terminal/ Surface contract + LoomIO + built-in WebSurface
|
|
16
|
+
(one import, runtime/ SessionRunner · Hub (multi-session) · snapshot accumulation · control verbs
|
|
17
|
+
internally agent/ driver registry + Claude/Echo drivers + spawn/parse ← Driver
|
|
18
|
+
modular) protocol/ UniversalSnapshot + diff (the wire vocabulary) ← Channel
|
|
19
|
+
ports/ SessionStore · ModelResolver · ToolProvider · ... (+ defaults)
|
|
20
|
+
▲ spawns external CLIs (unchanged)
|
|
21
|
+
下层 (unchanged): the `claude` / `codex` / … binaries, native protocols
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**IM and Web are not two systems** — they are both just `Surface`s over the same `LoomIO`.
|
|
25
|
+
|
|
26
|
+
## Quickstart
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { createLoom, ClaudeDriver, WebSurface } from '@pikiloom/kernel';
|
|
30
|
+
|
|
31
|
+
const loom = createLoom({
|
|
32
|
+
drivers: [new ClaudeDriver()], // 下层 (unchanged)
|
|
33
|
+
surfaces: [new WebSurface({ port: 8787 })], // 上层 (IM / Web / tunnel)
|
|
34
|
+
});
|
|
35
|
+
await loom.start();
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Add an IM terminal by implementing `Surface.start(io)`: route inbound messages to
|
|
39
|
+
`io.prompt(...)` and render `io.subscribe(...)` snapshots back. Everything else (storage,
|
|
40
|
+
credentials, tools, prompts) has a default and is overridable via ports.
|
|
41
|
+
|
|
42
|
+
### TUI passthrough (`pikiloom code` → Claude/Codex TUI)
|
|
43
|
+
|
|
44
|
+
Two orthogonal driver outputs off the same registry: `run()` yields a structured
|
|
45
|
+
`UniversalSnapshot` (Web/IM); `tui()` yields a raw, full-screen interactive process that
|
|
46
|
+
the kernel spawns in a **PTY** and passes through transparently (with optional tee/mirror).
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { createLoom, ClaudeDriver, CodexDriver } from '@pikiloom/kernel';
|
|
50
|
+
const loom = createLoom({ drivers: [new ClaudeDriver(), new CodexDriver()], defaultAgent: 'claude' });
|
|
51
|
+
await loom.runTui({ agent: 'claude', workdir: process.cwd() }); // you're now in the real Claude TUI, launched by the kernel
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`runTui` does full stdin/stdout raw passthrough on the current terminal; `openTui` returns
|
|
55
|
+
a `PtyBridge` (`onData`/`write`/`resize`/`onExit`) so you can drive or tee it yourself.
|
|
56
|
+
Requires the optional `node-pty` dependency.
|
|
57
|
+
|
|
58
|
+
## Contracts
|
|
59
|
+
|
|
60
|
+
| Seam | Interface | Direction |
|
|
61
|
+
|------|-----------|-----------|
|
|
62
|
+
| Agent (下层) | `AgentDriver.run(input, ctx)` → emits `DriverEvent`s | down, bundled (+ `registerDriver` escape hatch) |
|
|
63
|
+
| Surface (上层) | `Surface.start(io: LoomIO)` | up, app-provided (WebSurface built-in) |
|
|
64
|
+
| Session API | `LoomIO` = prompt/stop/steer/interact + subscribe/getSnapshot/listSessions | the meeting point |
|
|
65
|
+
| Plugin | `Plugin.tools()` / `decorateSnapshot()` | up, app-provided |
|
|
66
|
+
| Ports | `SessionStore` · `ModelResolver` · `ToolProvider` · `SystemPromptBuilder` · `InteractionHandler` | side, defaults included |
|
|
67
|
+
|
|
68
|
+
Wire shape: `UniversalSnapshot` (phase/text/reasoning/plan/toolCalls/usage/interactions/artifacts)
|
|
69
|
+
+ delta `diffSnapshot`/`applySnapshotPatch` (prefix-append text, `undefined→null` field-clears
|
|
70
|
+
that survive JSON).
|
|
71
|
+
|
|
72
|
+
## What's included
|
|
73
|
+
|
|
74
|
+
- Drivers: `EchoDriver` (hermetic), `ClaudeDriver` (real `claude` CLI, stream-json + TUI), `CodexDriver` (app-server JSON-RPC + TUI), `GeminiDriver` (stream-json), `HermesDriver` (ACP).
|
|
75
|
+
- Terminals: `WebSurface` (ws host speaking the wire protocol — any pikichannel client connects), `CliSurface`.
|
|
76
|
+
- TUI: `PtyBridge` + `attachTui` + `Loom.runTui/openTui` (raw PTY passthrough with tee).
|
|
77
|
+
- Ports: `FsSessionStore`, `NullModelResolver`, `NoopToolProvider`, `PassthroughSystemPromptBuilder`, `AutoCancelInteractionHandler`.
|
|
78
|
+
|
|
79
|
+
## Publishing / CI
|
|
80
|
+
|
|
81
|
+
`npm publish ./packages/kernel --access public` (scoped-public). Each pikiloom release bumps
|
|
82
|
+
+ builds the kernel (`scripts/release.sh`) and the Release workflow publishes it alongside
|
|
83
|
+
pikiloom — needs the `@pikiloom` npm scope to exist and `NPM_TOKEN` to have publish access.
|
|
84
|
+
|
|
85
|
+
> Known rough edge: node-pty's prebuilt `spawn-helper` can lose its executable bit on some
|
|
86
|
+
> npm installs (`posix_spawnp failed`). Fix: `chmod +x node_modules/node-pty/prebuilds/*/spawn-helper`.
|
|
87
|
+
|
|
88
|
+
## Verification
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npm run typecheck # tsc, clean
|
|
92
|
+
npm test # hermetic: snapshot diff, full lifecycle (stop/steer/interact), web ws flow
|
|
93
|
+
KERNEL_E2E_REAL=1 npm test # also drives the real `claude` CLI end-to-end
|
|
94
|
+
node examples/smoke.mjs # smoke against the compiled dist
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Status: contracts + runtime + Echo/Claude drivers + Web terminal + default ports are
|
|
98
|
+
implemented and **E2E-verified** (hermetic + real-claude + compiled-artifact). The kernel
|
|
99
|
+
lives beside `src/` and does **not** touch the existing pikiloom app.
|
|
100
|
+
|
|
101
|
+
## Roadmap (gated cutover)
|
|
102
|
+
|
|
103
|
+
1. Port the remaining drivers (Codex/Gemini/Hermes-ACP) behind the same `AgentDriver` contract.
|
|
104
|
+
2. Promote pikiloom's concrete IM channels to reference `Surface` adapters (`@pikiloom/kernel/surfaces/*`).
|
|
105
|
+
3. Add the full pikichannel transport (WebRTC + rendezvous + TURN + `/api` tunnel) as a `WebSurface` option.
|
|
106
|
+
4. Re-point the pikiloom app's `main.ts` at `createLoom({...})`; keep multi-session queue / promotion / goal
|
|
107
|
+
auto-continuation in the app (not the kernel). Switch over only after parity is proven on DEV.
|