pikiloom 0.4.38 → 0.4.40

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.
Files changed (36) hide show
  1. package/dashboard/dist/assets/{AgentTab-DbFzaIyZ.js → AgentTab-Cj7sekcS.js} +1 -1
  2. package/dashboard/dist/assets/{ConnectionModal-QNYOWHCU.js → ConnectionModal-g_6IHAWE.js} +1 -1
  3. package/dashboard/dist/assets/{DirBrowser-BGaKHjFT.js → DirBrowser-DVe6q12N.js} +1 -1
  4. package/dashboard/dist/assets/{ExtensionsTab-D4Gv9b8z.js → ExtensionsTab-GqxD61Pp.js} +1 -1
  5. package/dashboard/dist/assets/{IMAccessTab-DxxoZd84.js → IMAccessTab-DIqTzZJ9.js} +1 -1
  6. package/dashboard/dist/assets/{Modal-Nm2e93s5.js → Modal-CIYrMwm0.js} +1 -1
  7. package/dashboard/dist/assets/{Modals-BwxZmU0U.js → Modals-B-Afu2rD.js} +1 -1
  8. package/dashboard/dist/assets/{Select-SGlII0yx.js → Select-B1UL27gw.js} +1 -1
  9. package/dashboard/dist/assets/SessionPanel-BHuG5hxj.js +1 -0
  10. package/dashboard/dist/assets/{SystemTab-BF6lIAYM.js → SystemTab-Bv1uT5-h.js} +1 -1
  11. package/dashboard/dist/assets/{index-DZiAiRNt.js → index-BOEW0RIx.js} +14 -14
  12. package/dashboard/dist/assets/index-Bthwt6K_.css +1 -0
  13. package/dashboard/dist/assets/{index-BP8R_bLT.js → index-qKexKdJl.js} +3 -3
  14. package/dashboard/dist/assets/{shared-S0kcs5yP.js → shared-624PaUfm.js} +1 -1
  15. package/dashboard/dist/index.html +2 -2
  16. package/dist/agent/drivers/codex.js +5 -3
  17. package/dist/agent/index.js +1 -1
  18. package/dist/agent/kernel-bridge.js +1 -0
  19. package/dist/agent/mcp/capabilities.js +39 -0
  20. package/dist/agent/stream.js +13 -1
  21. package/dist/bot/bot.js +4 -13
  22. package/dist/dashboard/routes/agents.js +8 -4
  23. package/package.json +1 -1
  24. package/packages/kernel/README.md +263 -65
  25. package/packages/kernel/dist/contracts/surface.d.ts +17 -0
  26. package/packages/kernel/dist/drivers/claude.d.ts +12 -1
  27. package/packages/kernel/dist/drivers/claude.js +69 -5
  28. package/packages/kernel/dist/drivers/codex.d.ts +28 -1
  29. package/packages/kernel/dist/drivers/codex.js +161 -16
  30. package/packages/kernel/dist/index.d.ts +1 -1
  31. package/packages/kernel/dist/runtime/hub.d.ts +3 -0
  32. package/packages/kernel/dist/runtime/hub.js +84 -10
  33. package/packages/kernel/dist/runtime/loom.d.ts +1 -0
  34. package/packages/kernel/dist/runtime/loom.js +4 -1
  35. package/dashboard/dist/assets/SessionPanel-DYfSlreh.js +0 -1
  36. package/dashboard/dist/assets/index-CtS48Jn-.css +0 -1
@@ -1 +1 @@
1
- import{j as s}from"./react-vendor-C7Sl8SE7.js";import{N as t,m as i,S as a}from"./index-BP8R_bLT.js";function o({primary:e,secondary:l,tertiary:n}){return s.jsxs("div",{className:"flex flex-col gap-2 pt-1 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between",children:[s.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e&&s.jsxs(i,{tone:"primary",onClick:e.onClick,disabled:e.disabled,children:[e.loading&&s.jsx(a,{}),e.label]}),l&&s.jsx(i,{tone:"secondary",onClick:l.onClick,disabled:l.disabled,children:l.label})]}),n&&s.jsx("div",{className:"text-xs leading-relaxed text-fg-4",children:n})]})}function c({children:e,className:l}){return s.jsx(t,{padding:"md",elevation:"flat",className:l,children:e})}export{o as A,c as S};
1
+ import{j as s}from"./react-vendor-C7Sl8SE7.js";import{N as t,m as i,S as a}from"./index-qKexKdJl.js";function o({primary:e,secondary:l,tertiary:n}){return s.jsxs("div",{className:"flex flex-col gap-2 pt-1 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between",children:[s.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e&&s.jsxs(i,{tone:"primary",onClick:e.onClick,disabled:e.disabled,children:[e.loading&&s.jsx(a,{}),e.label]}),l&&s.jsx(i,{tone:"secondary",onClick:l.onClick,disabled:l.disabled,children:l.label})]}),n&&s.jsx("div",{className:"text-xs leading-relaxed text-fg-4",children:n})]})}function c({children:e,className:l}){return s.jsx(t,{padding:"md",elevation:"flat",className:l,children:e})}export{o as A,c as S};
@@ -6,10 +6,10 @@
6
6
  <link rel="icon" type="image/png" href="/logo.png">
7
7
  <title>Pikiloom</title>
8
8
  <link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
9
- <script type="module" crossorigin src="/assets/index-BP8R_bLT.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-qKexKdJl.js"></script>
10
10
  <link rel="modulepreload" crossorigin href="/assets/react-vendor-C7Sl8SE7.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/router-DHISdpPk.js">
12
- <link rel="stylesheet" crossorigin href="/assets/index-CtS48Jn-.css">
12
+ <link rel="stylesheet" crossorigin href="/assets/index-Bthwt6K_.css">
13
13
  </head>
14
14
  <body>
15
15
  <div id="root"></div>
@@ -34,9 +34,11 @@ export class CodexAppServer {
34
34
  return new Promise((resolve) => {
35
35
  const timer = setTimeout(() => { this.kill(); resolve(false); }, CODEX_APPSERVER_SPAWN_TIMEOUT_MS);
36
36
  const args = ['app-server'];
37
- const overrides = this.configOverrides.some(entry => /^features\.goals\s*=/.test(entry))
38
- ? this.configOverrides
39
- : [...this.configOverrides, 'features.goals=true'];
37
+ // Do NOT force model_reasoning_summary — reasoning summaries stay OFF by default (respecting
38
+ // ~/.codex/config.toml = original behavior). A caller wanting thinking passes it via configOverrides.
39
+ const overrides = [...this.configOverrides];
40
+ if (!overrides.some(entry => /^features\.goals\s*=/.test(entry)))
41
+ overrides.push('features.goals=true');
40
42
  for (const c of overrides)
41
43
  args.push('-c', c);
42
44
  agentLog(`[codex-rpc] spawning: codex ${args.join(' ')}`);
@@ -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, recoverProfileIdForModel, listModels, resolveAgentModels, getUsage, getAgentBoundModelId, setAgentBoundModelId, } from './stream.js';
10
+ export { detectAgentBin, listAgents, resolveDefaultAgent, run, doStream, recoverProfileIdForModel, listModels, resolveAgentModels, dropNativeShadowedByProfiles, 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';
@@ -135,6 +135,7 @@ export async function kernelStream(opts) {
135
135
  cachedInputTokens: s.usage?.cachedInputTokens ?? null,
136
136
  contextUsedTokens: s.usage?.contextUsedTokens ?? null,
137
137
  contextPercent: s.usage?.contextPercent ?? null,
138
+ turnOutputTokens: s.usage?.turnOutputTokens ?? null,
138
139
  toolCalls: s.toolCalls?.length ? s.toolCalls : undefined,
139
140
  subAgents: s.subAgents?.length ? s.subAgents : undefined,
140
141
  providerName: opts.byokProviderName ?? null,
@@ -0,0 +1,39 @@
1
+ // Session tool capabilities: each co-locates a session MCP tool group (defined under
2
+ // ./tools/) with the prompt fragment that teaches the agent to use it. This is the local,
3
+ // decoupled analog of a @pikiloom/kernel Plugin (a capability = its tools + its usage
4
+ // prompt, as one unit). pikiloom composes its own system prompt app-side and feeds the
5
+ // kernel a finished string, so this module intentionally does NOT import the kernel — that
6
+ // is what keeps pikiloom fully decoupled from the kernel's Hub/plugin runtime.
7
+ const ARTIFACT_RETURN = [
8
+ '[Artifact Return]',
9
+ 'To hand a file to the user — a screenshot, report, archive, generated asset, anything they asked you to "send" — call the `im_send_file` tool with the file path and a short caption. It is delivered through whatever terminal the user is on (an IM chat or the web dashboard) and stays retrievable even when they are connected remotely. Do NOT just print a local filesystem path: a remote user cannot open paths on this machine.',
10
+ ].join('\n');
11
+ const ASK_USER = [
12
+ '[Asking the user]',
13
+ 'The built-in `AskUserQuestion` tool is disabled here and will fail. If you would otherwise call it, call `mcp__pikiloom__im_ask_user` instead — same intent (a question plus optional choices), it blocks until the user replies via the IM/dashboard channel. Default behaviour is unchanged: infer obvious decisions yourself and only ask when you genuinely cannot proceed.',
14
+ ].join('\n');
15
+ export const SESSION_TOOL_CAPABILITIES = [
16
+ {
17
+ id: 'artifact-delivery',
18
+ tools: ['im_send_file', 'im_list_files'],
19
+ promptFragment: () => ARTIFACT_RETURN,
20
+ },
21
+ {
22
+ id: 'ask-user',
23
+ tools: ['im_ask_user'],
24
+ // Only when the HITL path is wired and the agent is claude (codex/gemini handle asks natively).
25
+ promptFragment: ({ agent, onInteraction }) => (onInteraction && agent === 'claude') ? ASK_USER : null,
26
+ },
27
+ ];
28
+ // Compose the session tool prompt = each capability's applicable fragment, in registration
29
+ // order, joined by a blank line. Byte-identical to the prior inline assembly in bot.ts.
30
+ export function composeSessionToolPrompt(ctx) {
31
+ const parts = [];
32
+ for (const cap of SESSION_TOOL_CAPABILITIES) {
33
+ const fragment = cap.promptFragment(ctx);
34
+ const trimmed = String(fragment || '').trim();
35
+ if (trimmed)
36
+ parts.push(trimmed);
37
+ }
38
+ return parts.join('\n\n');
39
+ }
@@ -658,6 +658,17 @@ export function listModels(agent, opts = {}) {
658
658
  function isLocalProviderBaseURL(baseURL) {
659
659
  return /^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(?::|\/|$)/i.test(baseURL);
660
660
  }
661
+ // A model surfaced as a BYOK profile must NOT also appear under the native group. Drivers seed
662
+ // their native list with the active model (via opts.currentModel) so it stays visible — but when
663
+ // a BYOK profile is active that "current model" is the profile's third-party model (e.g. a 豆包 /
664
+ // deepseek id), which then wrongly shows up as an "official / 官方 / Native" model. The profile row
665
+ // already represents it, so drop any native entry shadowed by a profile id.
666
+ export function dropNativeShadowedByProfiles(native, profileRows) {
667
+ if (!profileRows.length)
668
+ return native;
669
+ const shadowed = new Set(profileRows.map(m => m.id.trim().toLowerCase()).filter(Boolean));
670
+ return native.filter(m => !shadowed.has(m.id.trim().toLowerCase()));
671
+ }
661
672
  export async function resolveAgentModels(agent, opts = {}) {
662
673
  const driver = getDriver(agent);
663
674
  let nativeResult;
@@ -667,7 +678,7 @@ export async function resolveAgentModels(agent, opts = {}) {
667
678
  catch {
668
679
  nativeResult = { agent, models: [], sources: [], note: null };
669
680
  }
670
- const native = nativeResult.models.map(m => ({
681
+ const nativeRaw = nativeResult.models.map(m => ({
671
682
  id: m.id,
672
683
  alias: m.alias,
673
684
  group: 'native',
@@ -693,6 +704,7 @@ export async function resolveAgentModels(agent, opts = {}) {
693
704
  (isLocal ? local : cloud).push(entry);
694
705
  }
695
706
  }
707
+ const native = dropNativeShadowedByProfiles(nativeRaw, [...cloud, ...local]);
696
708
  const sources = [...nativeResult.sources];
697
709
  if (cloud.length)
698
710
  sources.push(`${cloud.length} cloud profile${cloud.length === 1 ? '' : 's'}`);
package/dist/bot/bot.js CHANGED
@@ -8,6 +8,7 @@ import { getActiveProfileId, getActiveProfile, setActiveProfile } from '../model
8
8
  import { querySessions, querySessionTail, updateSession, } from './session-hub.js';
9
9
  import { getDriver, hasDriver, allDriverIds, getDriverCapabilities } from '../agent/driver.js';
10
10
  import { resolveGuiIntegrationConfig } from '../agent/mcp/bridge.js';
11
+ import { composeSessionToolPrompt } from '../agent/mcp/capabilities.js';
11
12
  import { terminateProcessTree } from '../core/process-control.js';
12
13
  import { expandTilde } from '../core/platform.js';
13
14
  import { VERSION } from '../core/version.js';
@@ -48,18 +49,6 @@ function appendExtraPrompt(base, extra) {
48
49
  return lhs;
49
50
  return `${lhs}\n\n${rhs}`;
50
51
  }
51
- function buildMcpDeliveryPrompt() {
52
- return [
53
- '[Artifact Return]',
54
- 'To hand a file to the user — a screenshot, report, archive, generated asset, anything they asked you to "send" — call the `im_send_file` tool with the file path and a short caption. It is delivered through whatever terminal the user is on (an IM chat or the web dashboard) and stays retrievable even when they are connected remotely. Do NOT just print a local filesystem path: a remote user cannot open paths on this machine.',
55
- ].join('\n');
56
- }
57
- function buildClaudeAskUserPrompt() {
58
- return [
59
- '[Asking the user]',
60
- 'The built-in `AskUserQuestion` tool is disabled here and will fail. If you would otherwise call it, call `mcp__pikiloom__im_ask_user` instead — same intent (a question plus optional choices), it blocks until the user replies via the IM/dashboard channel. Default behaviour is unchanged: infer obvious decisions yourself and only ask when you genuinely cannot proceed.',
61
- ].join('\n');
62
- }
63
52
  function buildBrowserAutomationPrompt(browserEnabled) {
64
53
  if (!browserEnabled) {
65
54
  return [
@@ -2022,7 +2011,9 @@ export class Bot {
2022
2011
  const workflowEnabled = cs.agent === 'claude' && (extras?.workflowEnabled ?? this.claudeWorkflowEnabled);
2023
2012
  const deliverySessionKey = ('key' in cs && typeof cs.key === 'string') ? cs.key : null;
2024
2013
  const wrappedSendFile = this.buildArtifactSendFile(cs.agent, deliverySessionKey, cs, mcpSendFile);
2025
- const mcpSystemPrompt = appendExtraPrompt(appendExtraPrompt(appendExtraPrompt(buildMcpDeliveryPrompt(), onInteraction && cs.agent === 'claude' ? buildClaudeAskUserPrompt() : ''), buildBrowserAutomationPrompt(browserEnabled)), workflowEnabled ? buildWorkflowOptInPrompt() : '');
2014
+ // Session tool capabilities (im_send_file / im_ask_user) bundle their usage prompt with
2015
+ // the tool itself — see agent/mcp/capabilities.ts. Browser/workflow remain session policy.
2016
+ const mcpSystemPrompt = appendExtraPrompt(appendExtraPrompt(composeSessionToolPrompt({ agent: cs.agent, onInteraction: !!onInteraction }), buildBrowserAutomationPrompt(browserEnabled)), workflowEnabled ? buildWorkflowOptInPrompt() : '');
2026
2017
  const effectiveSystemPrompt = isFirstTurnOfSession
2027
2018
  ? appendExtraPrompt(systemPrompt, mcpSystemPrompt)
2028
2019
  : (mcpSystemPrompt || undefined);
@@ -137,6 +137,11 @@ async function buildAgentStatusResponse(config = loadUserConfig(), agentOptions
137
137
  }
138
138
  const runtimeSelectedModel = runtime.getRuntimeModel(agentId, config);
139
139
  const runtimeSelectedEffort = runtime.getRuntimeEffort(agentId, config);
140
+ const activeProfile = getActiveProfile(agentId);
141
+ // runtimeSelectedModel is the model that will RUN (profile-aware): when a BYOK profile is
142
+ // bound it is the profile's third-party model. The NATIVE model list must not be seeded with
143
+ // it, else it shows under the "官方 / Native" group (it is surfaced separately via byokModels).
144
+ const nativeRuntimeModel = activeProfile ? '' : runtimeSelectedModel;
140
145
  let models = [];
141
146
  let usage = emptyUsage(agentId, 'Agent not installed.');
142
147
  let nativeConfig = null;
@@ -149,10 +154,10 @@ async function buildAgentStatusResponse(config = loadUserConfig(), agentOptions
149
154
  }
150
155
  catch { }
151
156
  }
152
- const modelFallback = runtimeSelectedModel ? [{ id: runtimeSelectedModel, alias: null }] : [];
157
+ const modelFallback = nativeRuntimeModel ? [{ id: nativeRuntimeModel, alias: null }] : [];
153
158
  const cachedUsage = driver.getUsage({ agent: agentId, model: runtimeSelectedModel });
154
159
  const [resolvedModels, resolvedUsage] = await Promise.all([
155
- withTimeoutFallback(driver.listModels({ workdir, currentModel: runtimeSelectedModel }).then(result => dedupeModels([
160
+ withTimeoutFallback(driver.listModels({ workdir, currentModel: nativeRuntimeModel || undefined }).then(result => dedupeModels([
156
161
  ...modelFallback,
157
162
  ...result.models,
158
163
  ])), AGENT_STATUS_MODELS_TIMEOUT_MS, modelFallback),
@@ -169,14 +174,13 @@ async function buildAgentStatusResponse(config = loadUserConfig(), agentOptions
169
174
  }
170
175
  }
171
176
  const updateState = getAgentUpdateState(agentId);
172
- const activeProfile = getActiveProfile(agentId);
173
177
  const byokProvider = activeProfile ? getProvider(activeProfile.providerId) : null;
174
178
  const byokProviderName = byokProvider?.name || null;
175
179
  const byokProfileName = activeProfile
176
180
  && activeProfile.name.trim().toLowerCase() !== activeProfile.modelId.trim().toLowerCase()
177
181
  ? activeProfile.name
178
182
  : null;
179
- const nativeSelectedModel = runtimeSelectedModel || nativeConfig?.model || null;
183
+ const nativeSelectedModel = nativeRuntimeModel || nativeConfig?.model || null;
180
184
  const nativeSelectedEffort = runtimeSelectedEffort || nativeConfig?.effort || null;
181
185
  const selectedModel = activeProfile?.modelId || nativeSelectedModel;
182
186
  const selectedEffort = activeProfile?.effort || nativeSelectedEffort;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pikiloom",
3
- "version": "0.4.38",
3
+ "version": "0.4.40",
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": {
@@ -1,107 +1,305 @@
1
1
  # @pikiloom/kernel
2
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.
3
+ Turn heterogeneous coding agents (**Claude Code · Codex · Gemini · ACP/Hermes**) into one
4
+ **uniform, accumulating session snapshot + a small set of control verbs**, exposed over
5
+ **pluggable surfaces** (IM, Web, tunnel, raw terminal). Environmental concerns (storage,
6
+ credentials, tools, prompts) are **injected ports** with working defaults.
6
7
 
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.
8
+ This is the reusable core that [pikiloom](https://github.com/xiaotonng/pikiloom) itself is
9
+ built on. Drop it into any project and stand up a "pikiloom-like" backend in a few lines
10
+ **you never parse a CLI's output or learn each agent's wire format**; you read one
11
+ `UniversalSnapshot` and call `prompt / stop / steer / interact`.
9
12
 
10
- ## The model (one package, three rings)
13
+ ```bash
14
+ npm i @pikiloom/kernel
15
+ ```
16
+
17
+ - Runtime dep: `ws`. Optional: `node-pty` (only for raw-TUI passthrough). Node ≥ 20, ESM-only.
18
+ - TypeScript types ship in the package (`dist/**/*.d.ts`). For an LLM-oriented summary, see [`llms.txt`](./llms.txt).
19
+
20
+ ---
21
+
22
+ ## Mental model
11
23
 
12
24
  ```
13
- 上层 (you write): IM bindings · Web UI/skin · Plugins ── implement Surface / Plugin
25
+ 上层 (you write): IM bindings · Web UI · Plugins ── implement Surface / Plugin
14
26
  ▲ 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
27
+ @pikiloom/kernel: contracts/ Driver · Surface · LoomIO · Ports
28
+ (one import, runtime/ SessionRunner · Hub (multi-session) · snapshot accumulation · control verbs
29
+ modular inside) drivers/ Claude / Codex / Gemini / Hermes / Echo the agent axis (下层)
30
+ surfaces/ WebSurface (ws host) · CliSurface
31
+ protocol/ UniversalSnapshot + diff ← the wire vocabulary
32
+ ports/ SessionStore · ModelResolver · ToolProvider · ... (+ defaults)
33
+ spawns the external `claude` / `codex` / `gemini` CLIs (unchanged)
34
+ 下层 (unchanged): the agent binaries, native protocols
22
35
  ```
23
36
 
24
- **IM and Web are not two systems** — they are both just `Surface`s over the same `LoomIO`.
37
+ **IM and Web are not two systems** — both are just `Surface`s over the same `LoomIO`.
38
+
39
+ ### Two rails, same driver registry
40
+
41
+ | Rail | Driver method | Output | Use for |
42
+ |------|---------------|--------|---------|
43
+ | **Structured** | `driver.run(input, ctx)` | streamed `UniversalSnapshot` (text, reasoning, tool activity, plan, usage) | IM, Web dashboards, any UI that renders structured turns |
44
+ | **Raw PTY** | `driver.tui(input)` | a real full-screen interactive process passed through a PTY | a local terminal app (`pikiloom code`-style); needs `node-pty` |
25
45
 
26
- ## Quickstart
46
+ ---
47
+
48
+ ## Quick start
49
+
50
+ ### 1. One turn through one agent — `runTurn` (the bridge primitive)
51
+
52
+ The smallest unit. No persistence, no multi-session — just run a turn and get a streamed
53
+ snapshot + final result. This is exactly what an existing app maps onto its own UI.
27
54
 
28
55
  ```ts
29
- import { createLoom, ClaudeDriver, WebSurface } from '@pikiloom/kernel';
56
+ import { runTurn, ClaudeDriver } from '@pikiloom/kernel';
57
+
58
+ const { result, snapshot } = await runTurn(
59
+ new ClaudeDriver(),
60
+ { prompt: 'Summarize package.json in one line', workdir: process.cwd(), effort: 'high' },
61
+ {
62
+ onSnapshot: (s) => {
63
+ // fires on every event — render live:
64
+ // s.text accumulated assistant output
65
+ // s.reasoning accumulated thinking (when the model/auth exposes it)
66
+ // s.activity human-readable execution trail ("Read foo.ts", "Run shell: npm test")
67
+ // s.toolCalls structured tool calls [{ name, summary, status }]
68
+ // s.plan, s.usage, s.artifacts, s.interactions
69
+ process.stdout.write(`\r${s.activity?.split('\n').at(-1) ?? ''}`);
70
+ },
71
+ onSteer: (steer) => { /* call steer('extra prompt') mid-turn */ },
72
+ signal: undefined, // pass an AbortSignal to stop the turn
73
+ },
74
+ );
75
+ console.log(result.ok, result.text, result.sessionId);
76
+ ```
77
+
78
+ ### 2. A full multi-session backend — `createLoom`
79
+
80
+ Adds persistence, a session hub, per-session queueing, discovery, and surfaces. Every port
81
+ has a default, so this runs with zero wiring.
82
+
83
+ ```ts
84
+ import { createLoom, ClaudeDriver, CodexDriver, WebSurface } from '@pikiloom/kernel';
30
85
 
31
86
  const loom = createLoom({
32
- drivers: [new ClaudeDriver()], // 下层 (unchanged)
33
- surfaces: [new WebSurface({ port: 8787 })], // 上层 (IM / Web / tunnel)
87
+ drivers: [new ClaudeDriver(), new CodexDriver()], // 下层 (unchanged binaries)
88
+ surfaces: [new WebSurface({ port: 8787 })], // 上层 (Web/tunnel; add your IM Surface here)
89
+ defaultAgent: 'claude',
90
+ // optional ports — override any one to swap storage / credentials / tools / prompts:
91
+ // sessionStore, modelResolver, toolProvider, systemPromptBuilder, catalog, interactionHandler
34
92
  });
35
93
  await loom.start();
94
+
95
+ // Drive it from anywhere via loom.io (LoomIO):
96
+ const { sessionKey, taskId } = await loom.io.prompt({ prompt: 'hello', agent: 'claude' });
97
+ const unsub = loom.io.subscribe((key, snapshot, patch, seq) => { /* render */ });
98
+ loom.io.steer(taskId, 'actually, do X instead');
99
+ loom.io.stop(sessionKey);
36
100
  ```
37
101
 
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.
102
+ To add an **IM channel**, implement `Surface.start(io)`: route inbound messages to
103
+ `io.prompt(...)` and render `io.subscribe(...)` snapshots back out. Nothing else changes.
104
+
105
+ ### 3. Raw TUI passthrough — `runTui` / `openTui`
106
+
107
+ ```ts
108
+ const loom = createLoom({ drivers: [new ClaudeDriver()], defaultAgent: 'claude' });
109
+ await loom.runTui({ agent: 'claude', workdir: process.cwd() }); // you're now in the real Claude TUI
110
+ ```
41
111
 
42
- ### TUI passthrough (`pikiloom code` Claude/Codex TUI)
112
+ `runTui` does full stdin/stdout raw passthrough on the current terminal. `openTui` returns a
113
+ `PtyBridge` (`onData` / `write` / `resize` / `onExit`) so you can drive or tee it yourself.
114
+ Requires the optional `node-pty` dependency (`ptyAvailable()` reports availability).
43
115
 
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).
116
+ ---
117
+
118
+ ## The data you read: `UniversalSnapshot`
119
+
120
+ One driver-agnostic shape for every agent. A surface renders *this* and never touches a
121
+ CLI's native format. It **accumulates** across a turn (text/reasoning append; structured
122
+ fields replace).
47
123
 
48
124
  ```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
125
+ interface UniversalSnapshot {
126
+ phase: 'idle' | 'queued' | 'streaming' | 'done';
127
+ taskId?: string | null;
128
+ sessionId?: string | null;
129
+ agent?: string | null;
130
+ model?: string | null;
131
+ effort?: string | null;
132
+ prompt?: string | null;
133
+
134
+ text?: string; // accumulated assistant output
135
+ reasoning?: string; // accumulated thinking (only when the model/auth streams it; see note)
136
+ activity?: string; // human-readable execution trail, one line per tool/subagent (kernel-derived)
137
+ plan?: UniversalPlan | null; // { explanation, steps:[{ text, status }] }
138
+ toolCalls?: UniversalToolCall[]; // structured: { id, name, summary, input?, result?, status }
139
+ subAgents?: UniversalSubAgent[]; // spawned sub-agents and their tools
140
+ usage?: UniversalUsage | null; // { inputTokens, outputTokens, cachedInputTokens, contextPercent, ... }
141
+ artifacts?: UniversalArtifact[]; // generated files/images { url|path, fileName, mime, kind }
142
+ interactions?: UniversalInteraction[]; // pending human-in-the-loop questions (answer via interact())
143
+ queued?: UniversalQueuedTask[]; // prompts waiting behind the running turn
144
+
145
+ error?: string | null;
146
+ incomplete?: boolean; // true if the turn was interrupted / errored
147
+ startedAt?: number;
148
+ updatedAt: number;
149
+ }
52
150
  ```
53
151
 
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.
152
+ Sub-shapes:
57
153
 
58
- ## Contracts
154
+ ```ts
155
+ interface UniversalToolCall { id: string; name: string; summary: string; input?: string | null; result?: string | null; status: 'running' | 'done' | 'failed'; }
156
+ interface UniversalPlan { explanation: string | null; steps: { text: string; status: 'pending' | 'inProgress' | 'completed' }[]; }
157
+ interface UniversalUsage { inputTokens: number | null; outputTokens: number | null; cachedInputTokens: number | null; contextUsedTokens?: number | null; contextPercent: number | null; providerName?: string | null; }
158
+ interface UniversalArtifact { url?: string; path?: string; fileName: string; mime?: string; kind: 'photo' | 'document'; caption?: string; }
159
+ interface UniversalInteraction { promptId: string; kind: 'user-input' | 'permission' | 'confirmation'; title: string; questions: { id: string; text: string; type?: 'text' | 'select'; choices?: { label: string; value?: string }[] }[]; }
160
+ ```
59
161
 
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 |
162
+ **Activity projection (kernel-owned).** Every driver emits *structured* tool calls; the
163
+ kernel's `SessionRunner` derives `snapshot.activity` from `toolCalls` + `subAgents` centrally
164
+ (`projectActivity`): one line per call with a status suffix `summary` while running,
165
+ `summary done` / `summary -> detail` on success, `summary failed: detail` on error. So every
166
+ surface gets a readable execution trail for free, and rich UIs can still use `toolCalls`
167
+ directly. You implement this **nowhere** it's a property of the snapshot.
168
+
169
+ **Reasoning note.** Plaintext thinking only appears when the agent actually streams it
170
+ (e.g. BYOK Anthropic API keys, Codex `item/reasoning`). Subscription/OAuth Claude withholds
171
+ plaintext extended-thinking (streams only an encrypted signature), so `reasoning` is empty
172
+ there — a platform behavior, not a kernel limitation. The Claude driver also captures
173
+ reasoning delivered as a complete block, not only as streamed deltas.
174
+
175
+ ### Streaming on the wire — `diffSnapshot` / `applySnapshotPatch`
176
+
177
+ `diffSnapshot(prev, next)` produces a compact `SnapshotPatch` (prefix-append for `text` /
178
+ `reasoning`; field replacement otherwise, with `undefined → null` so field-clears survive
179
+ `JSON.stringify`). `applySnapshotPatch(prev, patch)` reassembles it on the client. `WebSurface`
180
+ uses these; reuse them for your own transport.
181
+
182
+ ---
183
+
184
+ ## Control verbs (`LoomIO`)
185
+
186
+ ```ts
187
+ io.prompt({ prompt, agent?, sessionKey?, workdir?, model?, effort?, attachments? }) // → { sessionKey, taskId }
188
+ io.stop(sessionKey) // interrupt the running turn (queued tasks stay & promote)
189
+ io.steer(taskId, prompt, attachments?) // inject a message mid-turn (drivers with capabilities.steer)
190
+ io.interact(promptId, action, value?) // answer a pending interaction: 'select' | 'text' | 'skip' | 'cancel'
191
+ io.subscribe((sessionKey, snapshot, patch, seq) => …) // live snapshots
192
+ io.getSnapshot(sessionKey) · io.getHistory(sessionKey) · io.listSessions()
193
+ io.listAgentInfo() · io.listModels(agent) · io.listEffort(agent, model?) · io.listTools(agent, workdir?) · io.listSkills(agent, workdir?)
194
+ ```
195
+
196
+ Concurrent prompts to one session **queue** by default (`serialPerSession`, no clobber); `stop`
197
+ interrupts only the current turn and the next queued task promotes.
198
+
199
+ ---
200
+
201
+ ## Drivers (the agent axis)
202
+
203
+ | Driver | id | transport | steer | interact | resume | tui |
204
+ |--------|----|-----------|:-----:|:--------:|:------:|:---:|
205
+ | `ClaudeDriver` | `claude` | `claude` CLI, stream-json (+ `--effort`, partial messages) | ✓ | — | ✓ | ✓ |
206
+ | `CodexDriver` | `codex` | `codex app-server` JSON-RPC (HITL via `requestUserInput`) | ✓ | via askUser | ✓ | ✓ |
207
+ | `GeminiDriver` | `gemini` | `gemini --output-format stream-json` | — | — | ✓ | ✓ |
208
+ | `HermesDriver` | `hermes` | ACP `session/update` | — | — | ✓ | — |
209
+ | `EchoDriver` | `echo` | none (hermetic, in-process) | ✓ | ✓ | ✓ | ✓ |
210
+
211
+ Write your own by implementing `AgentDriver` and passing it to `createLoom({ drivers })` (or
212
+ `loom.registerDriver(...)`). A driver normalizes its agent into `DriverEvent`s:
213
+
214
+ ```ts
215
+ type DriverEvent =
216
+ | { type: 'session'; sessionId: string }
217
+ | { type: 'text'; delta: string }
218
+ | { type: 'reasoning'; delta: string }
219
+ | { type: 'tool'; call: UniversalToolCall } // emit on tool start AND completion (status update)
220
+ | { type: 'plan'; plan: UniversalPlan }
221
+ | { type: 'subagent'; subagent: UniversalSubAgent }
222
+ | { type: 'usage'; usage: Partial<UniversalUsage> }
223
+ | { type: 'artifact'; artifact: UniversalArtifact }
224
+ | { type: 'activity'; line: string }; // explicit activity line (drivers that don't emit tool events)
225
+
226
+ interface AgentDriver {
227
+ readonly id: string;
228
+ readonly capabilities?: { steer?: boolean; interact?: boolean; resume?: boolean; tui?: boolean };
229
+ run(input: AgentTurnInput, ctx: DriverContext): Promise<DriverResult>; // ctx: { signal, emit, askUser, registerSteer }
230
+ tui?(input: TuiInput): TuiSpec;
231
+ }
232
+ ```
233
+
234
+ ---
235
+
236
+ ## Surfaces & Ports
237
+
238
+ **Surfaces** (上层) bind to `LoomIO`. Built in: `WebSurface` (a WebSocket host speaking the
239
+ wire protocol — any pikichannel-style client connects), `CliSurface`. Implement `Surface` to
240
+ add an IM channel or your own UI.
241
+
242
+ **Ports** (side) — all optional, each with a default, so `createLoom()` runs with zero config:
243
+
244
+ | Port | Default | Swap to… |
245
+ |------|---------|----------|
246
+ | `SessionStore` | `FsSessionStore` (`~/.<ns>/sessions`) | your DB / transcript store |
247
+ | `ModelResolver` | `NullModelResolver` (native login) | BYOK credential/provider injection |
248
+ | `ToolProvider` | `NoopToolProvider` | per-session MCP servers |
249
+ | `SystemPromptBuilder` | `PassthroughSystemPromptBuilder` | your system/developer prompt |
250
+ | `Catalog` | `NoopCatalog` | model/effort/tool/skill discovery for composers |
251
+ | `InteractionHandler` | `DeferToTerminalInteractionHandler` | programmatic HITL answers (`AutoCancelInteractionHandler` for one-shots) |
252
+
253
+ **Plugins** are the registration unit for everything ONE capability adds to a session —
254
+ register many, composed deterministically (and dynamically via `loom.registerPlugin(...)`):
255
+
256
+ ```ts
257
+ interface Plugin {
258
+ id: string;
259
+ tools?(opts: { agent; workdir }): McpServerSpec[]; // MCP servers
260
+ promptFragment?(opts: { agent; workdir; isFirstTurn }): string | null; // how-to-use / behavior prompt
261
+ contributeSpawn?(opts: { agent; workdir; mode: 'run'|'tui'; sessionId?; model? }): SpawnContribution | null; // { env?, extraArgs?, configOverrides? }
262
+ decorateSnapshot?(snapshot): UniversalSnapshot;
263
+ }
264
+ ```
67
265
 
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).
266
+ The kernel merges contributions per spawn — **never mutating global `process.env`** — in order
267
+ `[ModelResolver ToolProvider.env → plugins (registration order)]`, so a plugin can override
268
+ the resolver (e.g. point an agent's `ANTHROPIC_BASE_URL` at a local proxy). `promptFragment`s are
269
+ appended to the `SystemPromptBuilder` base and delivered via each agent's native mechanism. This
270
+ is how a capability registers its tools **and** their usage prompt **and** any env/flags in one
271
+ place — and how a model-traffic interceptor injects a redirect on both the `run()` and `tui()`
272
+ rails without the kernel knowing anything about it. (The singular `ModelResolver` /
273
+ `SystemPromptBuilder` ports remain the one authoritative model-credential / base-prompt source;
274
+ plugins are the composable per-capability layer on top.)
71
275
 
72
- ## What's included
276
+ ---
73
277
 
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`.
278
+ ## Exports
78
279
 
79
- ## Publishing / CI
280
+ Main entry `@pikiloom/kernel` re-exports everything. Subpaths: `@pikiloom/kernel/drivers`,
281
+ `@pikiloom/kernel/surfaces`, `@pikiloom/kernel/protocol`.
80
282
 
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.
283
+ - Runtime: `createLoom`, `Loom`, `Hub`, `SessionRunner`, `runTurn`, `PtyBridge`, `ptyAvailable`, `attachTui`
284
+ - Drivers: `EchoDriver`, `ClaudeDriver`, `CodexDriver`, `GeminiDriver`, `HermesDriver`
285
+ - Surfaces: `WebSurface`, `CliSurface`
286
+ - Ports/defaults: `FsSessionStore`, `NullModelResolver`, `NoopToolProvider`, `PassthroughSystemPromptBuilder`, `AutoCancelInteractionHandler`, `DeferToTerminalInteractionHandler`, `NoopCatalog`, `defaultBaseDir`
287
+ - Protocol: `UniversalSnapshot`, `diffSnapshot`, `applySnapshotPatch`, `emptySnapshot`, `PROTOCOL_VERSION`, all wire/`Client*`/`Server*` message types
288
+ - Types: `AgentDriver`, `AgentTurnInput`, `DriverContext`, `DriverEvent`, `DriverResult`, `LoomIO`, `PromptInput`, `Surface`, `Plugin`, `SpawnContribution`, `SessionStore`, `ModelResolver`, `ToolProvider`, `SystemPromptBuilder`, `InteractionHandler`, `Catalog`, …
84
289
 
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`.
290
+ ---
87
291
 
88
- ## Verification
292
+ ## Verify
89
293
 
90
294
  ```bash
91
295
  npm run typecheck # tsc, clean
92
- npm test # hermetic: snapshot diff, full lifecycle (stop/steer/interact), web ws flow
296
+ npm test # hermetic: snapshot diff, full lifecycle (stop/steer/interact), web ws flow, driver parsers
93
297
  KERNEL_E2E_REAL=1 npm test # also drives the real `claude` CLI end-to-end
94
298
  node examples/smoke.mjs # smoke against the compiled dist
95
299
  ```
96
300
 
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.
301
+ See `examples/` for a runnable web console, a Feishu/Lark terminal, and a Node smoke test.
100
302
 
101
- ## Roadmap (gated cutover)
303
+ ## License
102
304
 
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.
305
+ MIT
@@ -65,11 +65,28 @@ export interface Surface {
65
65
  start(io: LoomIO, host?: TuiHost): Promise<void>;
66
66
  stop(): Promise<void>;
67
67
  }
68
+ export interface SpawnContribution {
69
+ env?: Record<string, string>;
70
+ extraArgs?: string[];
71
+ configOverrides?: string[];
72
+ }
68
73
  export interface Plugin {
69
74
  readonly id: string;
70
75
  tools?(opts: {
71
76
  agent: string;
72
77
  workdir: string;
73
78
  }): McpServerSpec[] | Promise<McpServerSpec[]>;
79
+ promptFragment?(opts: {
80
+ agent: string;
81
+ workdir: string;
82
+ isFirstTurn: boolean;
83
+ }): string | null | undefined | Promise<string | null | undefined>;
84
+ contributeSpawn?(opts: {
85
+ agent: string;
86
+ workdir: string;
87
+ mode: 'run' | 'tui';
88
+ sessionId?: string | null;
89
+ model?: string | null;
90
+ }): SpawnContribution | null | undefined | Promise<SpawnContribution | null | undefined>;
74
91
  decorateSnapshot?(snapshot: UniversalSnapshot): UniversalSnapshot;
75
92
  }