@geminixiang/mikan 0.5.2 → 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +3 -3
  3. package/dist/adapter.d.ts +1 -1
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/adapter.js.map +1 -1
  6. package/dist/adapters/shared.js +2 -2
  7. package/dist/adapters/shared.js.map +1 -1
  8. package/dist/agent.d.ts +11 -2
  9. package/dist/agent.d.ts.map +1 -1
  10. package/dist/agent.js +211 -119
  11. package/dist/agent.js.map +1 -1
  12. package/dist/commands/admin.d.ts.map +1 -1
  13. package/dist/commands/admin.js +1 -5
  14. package/dist/commands/admin.js.map +1 -1
  15. package/dist/commands/auto-reply.d.ts +3 -0
  16. package/dist/commands/auto-reply.d.ts.map +1 -1
  17. package/dist/commands/auto-reply.js +3 -0
  18. package/dist/commands/auto-reply.js.map +1 -1
  19. package/dist/commands/model.d.ts +2 -3
  20. package/dist/commands/model.d.ts.map +1 -1
  21. package/dist/commands/model.js +1 -4
  22. package/dist/commands/model.js.map +1 -1
  23. package/dist/commands/new.d.ts.map +1 -1
  24. package/dist/commands/new.js +1 -5
  25. package/dist/commands/new.js.map +1 -1
  26. package/dist/commands/registry.d.ts.map +1 -1
  27. package/dist/commands/registry.js +2 -5
  28. package/dist/commands/registry.js.map +1 -1
  29. package/dist/commands/types.d.ts +6 -2
  30. package/dist/commands/types.d.ts.map +1 -1
  31. package/dist/commands/types.js.map +1 -1
  32. package/dist/config.d.ts +42 -1
  33. package/dist/config.d.ts.map +1 -1
  34. package/dist/config.js +93 -7
  35. package/dist/config.js.map +1 -1
  36. package/dist/events.d.ts +0 -1
  37. package/dist/events.d.ts.map +1 -1
  38. package/dist/events.js +1 -6
  39. package/dist/events.js.map +1 -1
  40. package/dist/execution-resolver.d.ts.map +1 -1
  41. package/dist/execution-resolver.js +9 -6
  42. package/dist/execution-resolver.js.map +1 -1
  43. package/dist/harness/auth.d.ts +16 -0
  44. package/dist/harness/auth.d.ts.map +1 -0
  45. package/dist/harness/auth.js +86 -0
  46. package/dist/harness/auth.js.map +1 -0
  47. package/dist/harness/extensions/loader.d.ts +43 -0
  48. package/dist/harness/extensions/loader.d.ts.map +1 -0
  49. package/dist/harness/extensions/loader.js +325 -0
  50. package/dist/harness/extensions/loader.js.map +1 -0
  51. package/dist/harness/extensions/registry.d.ts +23 -0
  52. package/dist/harness/extensions/registry.d.ts.map +1 -0
  53. package/dist/harness/extensions/registry.js +45 -0
  54. package/dist/harness/extensions/registry.js.map +1 -0
  55. package/dist/harness/extensions/types.d.ts +225 -0
  56. package/dist/harness/extensions/types.d.ts.map +1 -0
  57. package/dist/harness/extensions/types.js +2 -0
  58. package/dist/harness/extensions/types.js.map +1 -0
  59. package/dist/harness/http.d.ts +16 -0
  60. package/dist/harness/http.d.ts.map +1 -0
  61. package/dist/harness/http.js +71 -0
  62. package/dist/harness/http.js.map +1 -0
  63. package/dist/harness/index.d.ts +19 -0
  64. package/dist/harness/index.d.ts.map +1 -0
  65. package/dist/harness/index.js +18 -0
  66. package/dist/harness/index.js.map +1 -0
  67. package/dist/harness/models.d.ts +34 -0
  68. package/dist/harness/models.d.ts.map +1 -0
  69. package/dist/harness/models.js +248 -0
  70. package/dist/harness/models.js.map +1 -0
  71. package/dist/harness/runner.d.ts +124 -0
  72. package/dist/harness/runner.d.ts.map +1 -0
  73. package/dist/harness/runner.js +436 -0
  74. package/dist/harness/runner.js.map +1 -0
  75. package/dist/harness/session-store.d.ts +77 -0
  76. package/dist/harness/session-store.d.ts.map +1 -0
  77. package/dist/harness/session-store.js +248 -0
  78. package/dist/harness/session-store.js.map +1 -0
  79. package/dist/harness/settings.d.ts +47 -0
  80. package/dist/harness/settings.d.ts.map +1 -0
  81. package/dist/harness/settings.js +29 -0
  82. package/dist/harness/settings.js.map +1 -0
  83. package/dist/harness/skills.d.ts +54 -0
  84. package/dist/harness/skills.d.ts.map +1 -0
  85. package/dist/harness/skills.js +203 -0
  86. package/dist/harness/skills.js.map +1 -0
  87. package/dist/harness/types.d.ts +32 -0
  88. package/dist/harness/types.d.ts.map +1 -0
  89. package/dist/harness/types.js +2 -0
  90. package/dist/harness/types.js.map +1 -0
  91. package/dist/index.d.ts +1 -0
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +1 -0
  94. package/dist/index.js.map +1 -1
  95. package/dist/log.d.ts.map +1 -1
  96. package/dist/log.js +11 -14
  97. package/dist/log.js.map +1 -1
  98. package/dist/main.js +67 -8
  99. package/dist/main.js.map +1 -1
  100. package/dist/model-registry.d.ts +2 -2
  101. package/dist/model-registry.d.ts.map +1 -1
  102. package/dist/model-registry.js.map +1 -1
  103. package/dist/provisioner.d.ts +1 -4
  104. package/dist/provisioner.d.ts.map +1 -1
  105. package/dist/provisioner.js +3 -8
  106. package/dist/provisioner.js.map +1 -1
  107. package/dist/runtime/agent-run-controller.d.ts.map +1 -1
  108. package/dist/runtime/agent-run-controller.js +6 -3
  109. package/dist/runtime/agent-run-controller.js.map +1 -1
  110. package/dist/runtime/conversation-runtime.js +1 -1
  111. package/dist/runtime/conversation-runtime.js.map +1 -1
  112. package/dist/runtime/types.d.ts +4 -3
  113. package/dist/runtime/types.d.ts.map +1 -1
  114. package/dist/runtime/types.js.map +1 -1
  115. package/dist/sandbox/container.d.ts +1 -0
  116. package/dist/sandbox/container.d.ts.map +1 -1
  117. package/dist/sandbox/container.js +2 -2
  118. package/dist/sandbox/container.js.map +1 -1
  119. package/dist/sandbox/host.d.ts.map +1 -1
  120. package/dist/sandbox/host.js +0 -4
  121. package/dist/sandbox/host.js.map +1 -1
  122. package/dist/sandbox/image.d.ts.map +1 -1
  123. package/dist/sandbox/image.js +0 -3
  124. package/dist/sandbox/image.js.map +1 -1
  125. package/dist/sandbox/index.d.ts.map +1 -1
  126. package/dist/sandbox/index.js +4 -1
  127. package/dist/sandbox/index.js.map +1 -1
  128. package/dist/sandbox/types.d.ts +2 -2
  129. package/dist/sandbox/types.d.ts.map +1 -1
  130. package/dist/sandbox/types.js.map +1 -1
  131. package/dist/sessions/agent-memory-file-manager.d.ts.map +1 -1
  132. package/dist/sessions/agent-memory-file-manager.js +8 -8
  133. package/dist/sessions/agent-memory-file-manager.js.map +1 -1
  134. package/dist/sessions/metadata.js +2 -2
  135. package/dist/sessions/metadata.js.map +1 -1
  136. package/dist/sessions/store.d.ts +3 -3
  137. package/dist/sessions/store.d.ts.map +1 -1
  138. package/dist/sessions/store.js +17 -43
  139. package/dist/sessions/store.js.map +1 -1
  140. package/dist/sessions/types.d.ts +1 -1
  141. package/dist/sessions/types.d.ts.map +1 -1
  142. package/dist/sessions/types.js.map +1 -1
  143. package/dist/store.d.ts +1 -0
  144. package/dist/store.d.ts.map +1 -1
  145. package/dist/store.js +18 -11
  146. package/dist/store.js.map +1 -1
  147. package/dist/tools/edit.d.ts.map +1 -1
  148. package/dist/tools/edit.js +1 -3
  149. package/dist/tools/edit.js.map +1 -1
  150. package/dist/tools/event.d.ts.map +1 -1
  151. package/dist/tools/event.js +7 -74
  152. package/dist/tools/event.js.map +1 -1
  153. package/dist/tools/read.d.ts.map +1 -1
  154. package/dist/tools/read.js +1 -3
  155. package/dist/tools/read.js.map +1 -1
  156. package/dist/tools/write.d.ts.map +1 -1
  157. package/dist/tools/write.js +3 -7
  158. package/dist/tools/write.js.map +1 -1
  159. package/dist/trigger.d.ts +2 -0
  160. package/dist/trigger.d.ts.map +1 -1
  161. package/dist/trigger.js +7 -13
  162. package/dist/trigger.js.map +1 -1
  163. package/dist/types.d.ts +21 -0
  164. package/dist/types.d.ts.map +1 -1
  165. package/dist/types.js.map +1 -1
  166. package/dist/vault/index.d.ts.map +1 -1
  167. package/dist/vault/index.js +7 -1
  168. package/dist/vault/index.js.map +1 -1
  169. package/dist/web/admin/portal.d.ts +1 -1
  170. package/dist/web/admin/portal.d.ts.map +1 -1
  171. package/dist/web/admin/portal.js +394 -73
  172. package/dist/web/admin/portal.js.map +1 -1
  173. package/dist/web/admin/provider-models.d.ts +2 -2
  174. package/dist/web/admin/provider-models.d.ts.map +1 -1
  175. package/dist/web/admin/provider-models.js +4 -2
  176. package/dist/web/admin/provider-models.js.map +1 -1
  177. package/dist/web/admin/store.d.ts.map +1 -1
  178. package/dist/web/admin/store.js +3 -12
  179. package/dist/web/admin/store.js.map +1 -1
  180. package/dist/web/admin/types.d.ts +7 -5
  181. package/dist/web/admin/types.d.ts.map +1 -1
  182. package/dist/web/admin/types.js.map +1 -1
  183. package/dist/web/login/oauth.d.ts.map +1 -1
  184. package/dist/web/login/oauth.js +41 -23
  185. package/dist/web/login/oauth.js.map +1 -1
  186. package/dist/web/login/store.d.ts +0 -4
  187. package/dist/web/login/store.d.ts.map +1 -1
  188. package/dist/web/login/store.js +3 -16
  189. package/dist/web/login/store.js.map +1 -1
  190. package/dist/web/server.js +2 -2
  191. package/dist/web/server.js.map +1 -1
  192. package/dist/web/session-view/portal.d.ts.map +1 -1
  193. package/dist/web/session-view/portal.js +5 -16
  194. package/dist/web/session-view/portal.js.map +1 -1
  195. package/dist/web/session-view/service.js +7 -13
  196. package/dist/web/session-view/service.js.map +1 -1
  197. package/dist/web/session-view/store.d.ts.map +1 -1
  198. package/dist/web/session-view/store.js +2 -7
  199. package/dist/web/session-view/store.js.map +1 -1
  200. package/dist/web/token-store.d.ts +2 -8
  201. package/dist/web/token-store.d.ts.map +1 -1
  202. package/dist/web/token-store.js +11 -12
  203. package/dist/web/token-store.js.map +1 -1
  204. package/package.json +6 -3
  205. package/dist/context.d.ts +0 -2
  206. package/dist/context.d.ts.map +0 -1
  207. package/dist/context.js +0 -2
  208. package/dist/context.js.map +0 -1
package/dist/agent.js CHANGED
@@ -1,10 +1,10 @@
1
- import { Agent } from "@earendil-works/pi-agent-core";
2
- import { AgentSession, AuthStorage, convertToLlm, DefaultResourceLoader, formatSkillsForPrompt, getAgentDir, loadSkillsFromDir, ModelRegistry, SettingsManager, } from "@earendil-works/pi-coding-agent";
1
+ import { DEFAULT_EVENT_BUDGET, defaultExtensionDirs, formatSkillsForPrompt, loadExtensions, loadSkillsFromDir, MikanAgentSession, MikanModels, } from "./harness/index.js";
2
+ import { createHash } from "crypto";
3
3
  import { existsSync, readFileSync } from "fs";
4
4
  import { mkdir, readFile, writeFile } from "fs/promises";
5
- import { homedir } from "os";
6
5
  import { join, posix } from "path";
7
6
  import { resolveConversationSettings } from "./config.js";
7
+ import { readEnv } from "./utils/env.js";
8
8
  import { ActorExecutionResolver } from "./execution-resolver.js";
9
9
  import * as log from "./log.js";
10
10
  import { reportUserFacingError } from "./observability/sentry.js";
@@ -13,6 +13,7 @@ import { createMountedRuntimePathContext } from "./sandbox/path-context.js";
13
13
  import { addLifecycleBreadcrumb, metricAttributes, updateActiveSpanAttribution, } from "./observability/sentry.js";
14
14
  import { AgentMemoryFileManager } from "./sessions/agent-memory-file-manager.js";
15
15
  import { extractSessionUuid, openManagedSession, } from "./sessions/store.js";
16
+ import { HostEventStore } from "./tools/event.js";
16
17
  import { createMikanTools } from "./tools/index.js";
17
18
  import * as Sentry from "@sentry/node";
18
19
  import { formatLocalTimestamp } from "./utils/date.js";
@@ -151,7 +152,7 @@ export function resolveTriggerAttribution(message) {
151
152
  return `@${message.userName}`;
152
153
  return undefined;
153
154
  }
154
- function buildSystemPrompt(workspacePath, conversationId, conversationKind, currentUserId, memory, sandboxConfig, platform, skills, isEventTrigger = false, triggerAttribution) {
155
+ function buildSystemPrompt(workspacePath, conversationId, conversationKind, currentUserId, memory, sandboxConfig, platform, skills) {
155
156
  const { workspaceRoot, conversationPath, scratchPath } = buildRuntimePaths(workspacePath, conversationId);
156
157
  const sandboxType = sandboxConfig.type;
157
158
  const isContainerLike = sandboxType === "container" || sandboxType === "image";
@@ -165,25 +166,11 @@ function buildSystemPrompt(workspacePath, conversationId, conversationKind, curr
165
166
  ? platform.users.map((u) => `${u.id}\t@${u.userName}\t${u.displayName}`).join("\n")
166
167
  : "(no users loaded)";
167
168
  const envDescription = buildEnvDescription(sandboxType, workspaceRoot);
168
- const eventTriggerInstructions = isEventTrigger
169
- ? `
170
- ## Event Trigger Mode
171
- - You are handling a scheduled/background event, not opening a brand new chat with a stranger.
172
- - Treat the incoming user message as a self-contained task prepared by an earlier run.
173
- - Complete the task directly. Avoid generic greetings, self-introductions, or boilerplate offers to help.
174
- - For reminders/follow-ups, prefer a short direct response that sounds like a continuation of prior intent.
175
- - If the event text includes tone, brevity, or language instructions, follow them literally.
176
- `
177
- : "";
178
- const attributionInstructions = triggerAttribution
179
- ? `
180
- ## Attribution
181
- Always end your final ${platform.name} response and any GitHub issue/PR comments or descriptions you write via tools with:
182
- _Triggered by ${triggerAttribution}_
183
-
184
- Do not add this to \`[SILENT]\` responses.
185
- `
186
- : "";
169
+ // Per-turn instructions (event-trigger mode, attribution) are delivered with
170
+ // the user message via buildTurnInstructions(), not baked in here, so this
171
+ // system prompt stays byte-stable across a conversation's turns and keeps the
172
+ // provider prompt cache warm (a changing system prefix invalidates the cache
173
+ // for the whole request, including the far larger conversation history).
187
174
  const slackBlockKitInstructions = platform.name === "slack"
188
175
  ? `
189
176
  ## Slack Rendering
@@ -202,7 +189,6 @@ Do not add this to \`[SILENT]\` responses.
202
189
  - Scoped/thread sessions use fixed files at \`${conversationPath}/sessions/<scope_id>.jsonl\` (for example \`${conversationPath}/sessions/1777386320.800769.jsonl\`).
203
190
  - If a user asks about something that should exist in conversation history but is not found in the current context window, do not answer "I don't know" or "I don't have that". Instead, search the thread session, top-level session, and \`log.jsonl\` before responding.
204
191
  - User messages include a \`[in-thread:TS]\` marker when sent from within a platform thread/reply (TS is the thread or parent message identifier). Without this marker, the message is a top-level conversation message.
205
- ${eventTriggerInstructions}
206
192
  ${platform.formattingGuide}${slackBlockKitInstructions}
207
193
 
208
194
  ## Platform IDs
@@ -290,7 +276,6 @@ Format: \`{"date":"...","ts":"...","user":"...","userName":"...","text":"...","i
290
276
  The log contains user messages and your final responses (not tool calls/results).
291
277
  Use \`log.jsonl\` for quick grep-style history. Use \`${conversationPath}/sessions/\` when you need structured turns, tool outputs, or thread/session lineage.
292
278
  ${isContainerLike || isFirecracker ? "Install jq: apt-get install jq" : ""}
293
- ${attributionInstructions}
294
279
  \`\`\`bash
295
280
  # Recent messages
296
281
  tail -30 log.jsonl | jq -c '{date: .date[0:19], user: (.userName // .user), text}'
@@ -318,6 +303,32 @@ ls -1 sessions/
318
303
  Each tool requires a "label" parameter (shown to user).
319
304
  `;
320
305
  }
306
+ /**
307
+ * Instructions that vary per turn (event-trigger mode, response attribution).
308
+ * These are delivered with the user message rather than baked into the system
309
+ * prompt, so the system prompt stays cache-stable across a conversation's turns
310
+ * (in a multi-user channel the attribution line alone changes every turn).
311
+ * Returns an empty string when the turn needs no special framing.
312
+ */
313
+ export function buildTurnInstructions(isEventTrigger, triggerAttribution, platformName) {
314
+ const parts = [];
315
+ if (isEventTrigger) {
316
+ parts.push(`## Event Trigger Mode
317
+ - You are handling a scheduled/background event, not opening a brand new chat with a stranger.
318
+ - Treat the incoming user message as a self-contained task prepared by an earlier run.
319
+ - Complete the task directly. Avoid generic greetings, self-introductions, or boilerplate offers to help.
320
+ - For reminders/follow-ups, prefer a short direct response that sounds like a continuation of prior intent.
321
+ - If the event text includes tone, brevity, or language instructions, follow them literally.`);
322
+ }
323
+ if (triggerAttribution) {
324
+ parts.push(`## Attribution
325
+ Always end your final ${platformName} response and any GitHub issue/PR comments or descriptions you write via tools with:
326
+ _Triggered by ${triggerAttribution}_
327
+
328
+ Do not add this to \`[SILENT]\` responses.`);
329
+ }
330
+ return parts.join("\n\n");
331
+ }
321
332
  export function getUnresolvedSandboxPathContext(sandboxConfig, hostWorkspaceRoot) {
322
333
  if (sandboxConfig.type === "image") {
323
334
  return createMountedRuntimePathContext(hostWorkspaceRoot, "/workspace");
@@ -325,13 +336,7 @@ export function getUnresolvedSandboxPathContext(sandboxConfig, hostWorkspaceRoot
325
336
  return createExecutor(sandboxConfig).getPathContext(hostWorkspaceRoot);
326
337
  }
327
338
  function createRunnerExecutionContext(sandboxConfig, vaultManager, provisioner, workspaceDir, hostWorkspacePath) {
328
- const executionResolver = vaultManager &&
329
- sandboxConfig.type !== "host" &&
330
- (vaultManager.isEnabled() ||
331
- sandboxConfig.type === "container" ||
332
- sandboxConfig.type === "image" ||
333
- sandboxConfig.type === "cloudflare" ||
334
- sandboxConfig.type === "firecracker")
339
+ const executionResolver = vaultManager && sandboxConfig.type !== "host"
335
340
  ? new ActorExecutionResolver(sandboxConfig, vaultManager, provisioner, workspaceDir)
336
341
  : undefined;
337
342
  // activeExecutor is replaced at the start of each run() call when executionResolver
@@ -364,58 +369,81 @@ function createRunnerExecutionContext(sandboxConfig, vaultManager, provisioner,
364
369
  },
365
370
  };
366
371
  }
367
- async function createConfiguredAgentSession(params) {
368
- const { conversationId, workspaceDir, runtimeWorkspaceRoot, systemPrompt, model, thinkingLevel, tools, sessionManager, settingsManager, modelRegistry, } = params;
369
- const agent = new Agent({
370
- initialState: {
371
- systemPrompt,
372
- model,
373
- thinkingLevel,
374
- tools,
372
+ /**
373
+ * Extension host services over mikan's runtime infrastructure: schedules
374
+ * become event files under `<workspaceDir>/events` (picked up live by
375
+ * EventsWatcher), secrets come from `vaults/extensions/<slug>/env`, and
376
+ * notify posts through the platform bots when main.ts provides a notifier.
377
+ */
378
+ function buildExtensionHostServices(params) {
379
+ const { workspaceDir, vaultManager, platformNotifier } = params;
380
+ const eventStore = HostEventStore.fromWorkspaceDir(workspaceDir);
381
+ return {
382
+ stateDir: readEnv("STATE_DIR"),
383
+ scheduleStore: {
384
+ write: async (filename, payload) => {
385
+ // Event files tolerate a missing platform (single-platform default),
386
+ // so the harness payload is a valid event payload as written.
387
+ await eventStore.write(filename, payload);
388
+ },
389
+ delete: async (filename) => (await eventStore.delete(filename)).deleted,
390
+ list: async () => (await eventStore.list()).map((entry) => ({
391
+ filename: entry.filename,
392
+ payload: entry.payload,
393
+ })),
375
394
  },
376
- convertToLlm,
377
- getApiKey: async (provider) => {
378
- const key = await modelRegistry.getApiKeyForProvider(provider);
379
- if (!key) {
380
- throw new Error(`No API key for provider "${provider}". Set the appropriate environment variable or configure via auth.json`);
395
+ ...(platformNotifier ? { postMessage: platformNotifier } : {}),
396
+ ...(vaultManager
397
+ ? {
398
+ resolveSecrets: (slug) => vaultManager.resolve(`extensions/${slug}`)?.env ?? {},
381
399
  }
382
- return key;
383
- },
400
+ : {}),
401
+ };
402
+ }
403
+ /**
404
+ * Local (workspace/conversation) skills override extension skills on name
405
+ * collision: an admin's on-disk skill always beats an extension's.
406
+ */
407
+ function mergeExtensionSkills(local, extension) {
408
+ if (extension.length === 0)
409
+ return local;
410
+ const byName = new Map();
411
+ for (const skill of extension)
412
+ byName.set(skill.name, skill);
413
+ for (const skill of local)
414
+ byName.set(skill.name, skill);
415
+ return [...byName.values()];
416
+ }
417
+ async function createConfiguredAgentSession(params) {
418
+ const { conversationId, workspaceDir, systemPrompt, model, thinkingLevel, tools, sessionStore, models, vaultManager, platformNotifier, } = params;
419
+ // Host-only dirs under the state dir: extension code runs in the mikan
420
+ // process, so it must never load from workspace paths — those are mounted
421
+ // into sandbox containers and agent-writable (sandbox escape otherwise).
422
+ const extensionsResult = await loadExtensions({
423
+ dirs: defaultExtensionDirs(conversationId, readEnv("STATE_DIR")),
424
+ context: { conversationId, workspaceDir, model, thinkingLevel },
425
+ services: buildExtensionHostServices({ workspaceDir, vaultManager, platformNotifier }),
384
426
  });
385
- const loadedSession = sessionManager.buildSessionContext();
386
- if (loadedSession.messages.length > 0) {
387
- agent.state.messages = loadedSession.messages;
388
- log.logInfo(`[${conversationId}] Reloaded ${loadedSession.messages.length} messages from session context`);
427
+ for (const err of extensionsResult.errors) {
428
+ log.logWarning(`[${conversationId}] Extension load error: ${err.path}`, err.error);
389
429
  }
390
- const resourceLoader = new DefaultResourceLoader({
391
- cwd: workspaceDir,
392
- agentDir: getAgentDir(),
430
+ if (extensionsResult.extensions.length > 0) {
431
+ log.logInfo(`[${conversationId}] Loaded ${extensionsResult.extensions.length} extension(s): ${extensionsResult.extensions.map((extension) => extension.name).join(", ")}`);
432
+ }
433
+ const session = new MikanAgentSession({
393
434
  systemPrompt,
435
+ model,
436
+ thinkingLevel,
437
+ tools,
438
+ models,
439
+ sessionStore,
440
+ extensions: extensionsResult.registry,
394
441
  });
395
- try {
396
- await resourceLoader.reload();
397
- const extResult = resourceLoader.getExtensions();
398
- if (extResult.errors.length > 0) {
399
- for (const err of extResult.errors) {
400
- log.logWarning(`[${conversationId}] Extension load error: ${err.path}`, err.error);
401
- }
402
- }
403
- log.logInfo(`[${conversationId}] Loaded ${extResult.extensions.length} extension(s): ${extResult.extensions.map((extension) => extension.path).join(", ")}`);
404
- }
405
- catch (error) {
406
- log.logWarning(`[${conversationId}] Failed to load resources`, String(error));
442
+ const reloaded = session.reloadFromSession();
443
+ if (reloaded > 0) {
444
+ log.logInfo(`[${conversationId}] Reloaded ${reloaded} messages from session context`);
407
445
  }
408
- const baseToolsOverride = Object.fromEntries(tools.map((tool) => [tool.name, tool]));
409
- const session = new AgentSession({
410
- agent,
411
- sessionManager,
412
- settingsManager,
413
- cwd: runtimeWorkspaceRoot,
414
- modelRegistry,
415
- resourceLoader,
416
- baseToolsOverride,
417
- });
418
- return { agent, session };
446
+ return { session, extensionSkills: extensionsResult.skills };
419
447
  }
420
448
  function createEmptyUsageTotals() {
421
449
  return {
@@ -541,13 +569,23 @@ function getFinalAssistantText(session) {
541
569
  .map((content) => content.text)
542
570
  .join("\n") || "");
543
571
  }
544
- export function appendTriggerAttribution(text, triggerAttribution) {
572
+ export function appendTriggerAttribution(text, triggerAttribution, sessionLink) {
545
573
  if (!triggerAttribution)
546
574
  return text;
547
- const suffix = `_Triggered by ${triggerAttribution}_`;
548
- if (text.trimEnd().endsWith(suffix))
575
+ const trimmed = text.trimEnd();
576
+ const legacySuffix = `_Triggered by ${triggerAttribution}_`;
577
+ const suffix = sessionLink
578
+ ? `_Triggered by ${triggerAttribution} · session: ${sessionLink}_`
579
+ : legacySuffix;
580
+ if (trimmed.endsWith(suffix))
549
581
  return text;
550
- return `${text.trimEnd()}\n\n${suffix}`;
582
+ const body = trimmed.endsWith(legacySuffix)
583
+ ? trimmed.slice(0, -legacySuffix.length).trimEnd()
584
+ : trimmed;
585
+ return `${body}\n\n${suffix}`;
586
+ }
587
+ function isEventTriggerAttribution(triggerAttribution) {
588
+ return triggerAttribution?.startsWith("[event:") === true;
551
589
  }
552
590
  function extractToolLabel(toolName, args) {
553
591
  const label = args?.label;
@@ -635,7 +673,7 @@ async function finalizeRunResponse(responder, session, runState, options) {
635
673
  if (!finalText.trim())
636
674
  return;
637
675
  try {
638
- await responder.replaceResponse(appendTriggerAttribution(formatResponseWithToolProgress(finalText, runState), options?.triggerAttribution), { createOverflowLink: options?.createOverflowLink });
676
+ await responder.replaceResponse(appendTriggerAttribution(formatResponseWithToolProgress(finalText, runState), options?.triggerAttribution, options?.triggerSessionLink), { createOverflowLink: options?.createOverflowLink });
639
677
  }
640
678
  catch (err) {
641
679
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -701,15 +739,14 @@ async function reportUsageSummary(ctx) {
701
739
  await waitForQueue();
702
740
  }
703
741
  }
704
- function reloadSessionMessages(sessionManager, conversationId, agent) {
705
- const messages = sessionManager.buildSessionContext().messages;
706
- if (messages.length > 0) {
707
- agent.state.messages = messages;
708
- log.logInfo(`[${conversationId}] Reloaded ${messages.length} messages from context`);
742
+ function reloadSessionMessages(session, conversationId) {
743
+ const reloaded = session.reloadFromSession();
744
+ if (reloaded > 0) {
745
+ log.logInfo(`[${conversationId}] Reloaded ${reloaded} messages from context`);
709
746
  }
710
747
  }
711
748
  async function prepareRunContext(params) {
712
- const { message, responder, platform, conversationId, conversationDir, sessionUuid, runState, executor, executionResolver, resolveExecutorForRun, getPathContext, sessionManager, session, agent, setEventContext, setSandboxContext, setUploadFunction, } = params;
749
+ const { message, responder, platform, conversationId, conversationDir, sessionUuid, runState, executor, executionResolver, resolveExecutorForRun, getPathContext, session, setEventContext, setSandboxContext, setUploadFunction, } = params;
713
750
  let pathContext = params.pathContext;
714
751
  const sessionConversation = message.sessionKey.split(":")[0];
715
752
  await mkdir(join(conversationDir, "scratch"), { recursive: true });
@@ -721,12 +758,19 @@ async function prepareRunContext(params) {
721
758
  });
722
759
  pathContext = getPathContext();
723
760
  }
724
- reloadSessionMessages(sessionManager, conversationId, agent);
761
+ reloadSessionMessages(session, conversationId);
725
762
  const memory = await getMemory(conversationDir);
726
- const skills = loadMikanSkills(conversationDir, pathContext.runtimeWorkspaceRoot);
763
+ const skills = mergeExtensionSkills(loadMikanSkills(conversationDir, pathContext.runtimeWorkspaceRoot), params.extensionSkills ?? []);
727
764
  const triggerAttribution = resolveTriggerAttribution(message);
728
- const systemPrompt = buildSystemPrompt(pathContext.runtimeWorkspaceRoot, conversationId, message.conversationKind, message.userId, memory, executor.getSandboxConfig(), platform, skills, message.id.startsWith("event:"), triggerAttribution);
765
+ const systemPrompt = buildSystemPrompt(pathContext.runtimeWorkspaceRoot, conversationId, message.conversationKind, message.userId, memory, executor.getSandboxConfig(), platform, skills);
729
766
  session.agent.state.systemPrompt = systemPrompt;
767
+ // Cache diagnosis: a byte-stable system prompt is the precondition for
768
+ // provider prompt caching. If this hash changes between turns of one
769
+ // conversation, something turn-varying leaked into the prompt; if it is
770
+ // stable and cacheRead stays 0, the miss is provider-side (e.g. OpenRouter
771
+ // routing the model across upstream hosts).
772
+ const promptHash = createHash("sha256").update(systemPrompt).digest("hex").slice(0, 8);
773
+ log.logInfo(`[${conversationId}] System prompt: ${systemPrompt.length} chars, sha ${promptHash}`);
730
774
  setEventContext({
731
775
  platform: platform.name,
732
776
  conversationId,
@@ -744,11 +788,13 @@ async function prepareRunContext(params) {
744
788
  log.logInfo(`Context sizes - system: ${systemPrompt.length} chars, memory: ${memory.length} chars`);
745
789
  log.logInfo(`Channels: ${platform.channels.length}, Users: ${platform.users.length}`);
746
790
  const { userMessage, imageAttachments } = buildPromptPayload(message, pathContext.runtimeWorkspaceRoot, pathContext);
747
- await writePromptDebugContext(conversationDir, systemPrompt, session, userMessage, imageAttachments.length);
791
+ const turnInstructions = buildTurnInstructions(message.id.startsWith("event:"), triggerAttribution, platform.name);
792
+ const finalUserMessage = turnInstructions ? `${turnInstructions}\n\n${userMessage}` : userMessage;
793
+ await writePromptDebugContext(conversationDir, systemPrompt, session, finalUserMessage, imageAttachments.length);
748
794
  return {
749
795
  sessionConversation,
750
796
  runQueue,
751
- userMessage,
797
+ userMessage: finalUserMessage,
752
798
  imageAttachments,
753
799
  triggerAttribution,
754
800
  pathContext,
@@ -760,6 +806,7 @@ function formatAgentActorName(userName, fallback) {
760
806
  function sendAgentEvent(payload) {
761
807
  emitAgentEvent({ source: "mikan", ...payload });
762
808
  }
809
+ // ponytail: additive SSE mirror only; keep responder rendering here until another frontend needs the same stream.
763
810
  function attachSessionEventHandlers(params) {
764
811
  const { session, runState, model, agentConfig } = params;
765
812
  session.subscribe(async (event) => {
@@ -860,12 +907,17 @@ function attachSessionEventHandlers(params) {
860
907
  }
861
908
  if (event.type === "message_update") {
862
909
  const assistantMessageEvent = event.assistantMessageEvent;
863
- if (assistantMessageEvent?.type === "text_delta" &&
864
- assistantMessageEvent.delta &&
865
- responder.appendResponseDelta) {
866
- queue.enqueue(async () => {
867
- await responder.appendResponseDelta?.(assistantMessageEvent.delta ?? "");
868
- }, "response delta");
910
+ if (assistantMessageEvent?.type === "text_delta" && assistantMessageEvent.delta) {
911
+ sendAgentEvent({
912
+ sessionId: agentEventSessionId,
913
+ actorName: formatAgentActorName(logCtx.userName, logCtx.conversationId),
914
+ event: { kind: "responseDelta", delta: assistantMessageEvent.delta },
915
+ });
916
+ if (responder.appendResponseDelta) {
917
+ queue.enqueue(async () => {
918
+ await responder.appendResponseDelta?.(assistantMessageEvent.delta ?? "");
919
+ }, "response delta");
920
+ }
869
921
  }
870
922
  return;
871
923
  }
@@ -948,6 +1000,11 @@ function attachSessionEventHandlers(params) {
948
1000
  return;
949
1001
  const finalText = appendTriggerAttribution(formatResponseWithToolProgress(text, runState), runState.triggerAttribution);
950
1002
  log.logResponse(logCtx, text);
1003
+ sendAgentEvent({
1004
+ sessionId: agentEventSessionId,
1005
+ actorName: formatAgentActorName(logCtx.userName, logCtx.conversationId),
1006
+ event: { kind: "responseFinal", text: finalText },
1007
+ });
951
1008
  sendAgentEvent({
952
1009
  sessionId: agentEventSessionId,
953
1010
  actorName: formatAgentActorName(logCtx.userName, logCtx.conversationId),
@@ -966,8 +1023,14 @@ function attachSessionEventHandlers(params) {
966
1023
  return;
967
1024
  }
968
1025
  if (event.type === "compaction_start") {
1026
+ const text = "_Compacting context..._";
969
1027
  log.logInfo(`Auto-compaction started (reason: ${event.reason})`);
970
- queue.enqueue(() => responder.respond("_Compacting context..._"), "compaction start");
1028
+ sendAgentEvent({
1029
+ sessionId: agentEventSessionId,
1030
+ actorName: formatAgentActorName(logCtx.userName, logCtx.conversationId),
1031
+ event: { kind: "diagnostic", text },
1032
+ });
1033
+ queue.enqueue(() => responder.respond(text), "compaction start");
971
1034
  return;
972
1035
  }
973
1036
  if (event.type === "compaction_end") {
@@ -986,7 +1049,23 @@ function attachSessionEventHandlers(params) {
986
1049
  actorName: formatAgentActorName(logCtx.userName, logCtx.conversationId),
987
1050
  event: { kind: "sessionStart" },
988
1051
  });
989
- queue.enqueue(() => responder.respond(`_Retrying (${event.attempt}/${event.maxAttempts})..._`), "retry");
1052
+ const text = `_Retrying (${event.attempt}/${event.maxAttempts})..._`;
1053
+ sendAgentEvent({
1054
+ sessionId: agentEventSessionId,
1055
+ actorName: formatAgentActorName(logCtx.userName, logCtx.conversationId),
1056
+ event: { kind: "diagnostic", text },
1057
+ });
1058
+ queue.enqueue(() => responder.respond(text), "retry");
1059
+ }
1060
+ if (event.type === "budget_exceeded") {
1061
+ log.logWarning("Run stopped by budget circuit breaker", `${event.reason} (tokens=${event.tokens}, cost=${event.costUsd.toFixed(2)}, calls=${event.llmCalls}, ${event.durationMs}ms)`);
1062
+ const text = `_Stopped: run budget exceeded (${event.reason})_`;
1063
+ sendAgentEvent({
1064
+ sessionId: agentEventSessionId,
1065
+ actorName: formatAgentActorName(logCtx.userName, logCtx.conversationId),
1066
+ event: { kind: "diagnostic", text },
1067
+ });
1068
+ queue.enqueue(() => responder.respondDiagnostic(text, { style: "error" }), "budget exceeded");
990
1069
  }
991
1070
  });
992
1071
  }
@@ -1018,15 +1097,17 @@ function extractToolResultText(result) {
1018
1097
  * Runner caching is handled by the caller (channelStates in main.ts).
1019
1098
  * This is a stateless factory function.
1020
1099
  */
1021
- export async function createRunner(sandboxConfig, sessionKey, conversationId, conversationDir, workspaceDir, sessionScope, vaultManager, provisioner, sessionView) {
1100
+ export async function createRunner(sandboxConfig, sessionKey, conversationId, conversationDir, workspaceDir, sessionScope, vaultManager, provisioner, sessionView, platformNotifier) {
1022
1101
  const agentConfig = resolveConversationSettings(conversationDir);
1023
1102
  const workspaceBase = join(conversationDir, "..");
1024
1103
  const { executionResolver, executor, getPathContext, resolveExecutorForRun } = createRunnerExecutionContext(sandboxConfig, vaultManager, provisioner, workspaceDir, workspaceBase);
1025
1104
  let pathContext = getUnresolvedSandboxPathContext(sandboxConfig, workspaceBase);
1026
1105
  // Create tools (per-runner, with per-runner upload function setter)
1027
1106
  const { tools, setUploadFunction, setEventContext, setSandboxContext } = createMikanTools(executor, workspaceDir, { sandbox: sandboxConfig, provisioner });
1028
- const authStorage = AuthStorage.create(join(homedir(), ".pi", "mikan", "auth.json"));
1029
- const modelRegistry = ModelRegistry.create(authStorage);
1107
+ const modelRegistry = MikanModels.create();
1108
+ if (modelRegistry.getError()) {
1109
+ log.logWarning("models.json load error", modelRegistry.getError());
1110
+ }
1030
1111
  const model = resolveConfiguredModel(modelRegistry, agentConfig.provider, agentConfig.model);
1031
1112
  // Initial system prompt (will be updated each run with fresh memory/channels/users/skills)
1032
1113
  const memory = await getMemory(conversationDir);
@@ -1042,26 +1123,25 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
1042
1123
  // use the conversation's current pointer; scoped sessions use fixed files.
1043
1124
  // Platform-specific scope behavior is resolved before runner creation.
1044
1125
  const isThread = sessionKey.includes(":");
1045
- const { sessionDir, contextFile, threadRootMessage } = sessionScope;
1046
- const sessionManager = openManagedSession(contextFile, sessionDir, pathContext.runtimeWorkspaceRoot);
1126
+ const { contextFile, threadRootMessage } = sessionScope;
1127
+ const sessionManager = openManagedSession(contextFile, pathContext.runtimeWorkspaceRoot);
1047
1128
  const threadSessionName = buildThreadSessionName(threadRootMessage);
1048
1129
  if (isThread && threadSessionName && sessionManager.getSessionName() !== threadSessionName) {
1049
1130
  sessionManager.appendSessionInfo(threadSessionName);
1050
1131
  }
1051
1132
  const sessionUuid = extractSessionUuid(contextFile);
1052
1133
  const chatSessionManager = new AgentMemoryFileManager();
1053
- const settingsManager = SettingsManager.inMemory();
1054
- const { agent, session } = await createConfiguredAgentSession({
1134
+ const { session, extensionSkills } = await createConfiguredAgentSession({
1055
1135
  conversationId,
1056
1136
  workspaceDir,
1057
- runtimeWorkspaceRoot: pathContext.runtimeWorkspaceRoot,
1058
1137
  systemPrompt,
1059
1138
  model,
1060
1139
  thinkingLevel: agentConfig.thinkingLevel,
1061
1140
  tools,
1062
- sessionManager,
1063
- settingsManager,
1064
- modelRegistry,
1141
+ sessionStore: sessionManager,
1142
+ models: modelRegistry,
1143
+ vaultManager,
1144
+ platformNotifier,
1065
1145
  });
1066
1146
  // Mutable per-run state - event handler references this
1067
1147
  const runState = createRunState();
@@ -1088,9 +1168,8 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
1088
1168
  executionResolver,
1089
1169
  resolveExecutorForRun,
1090
1170
  getPathContext,
1091
- sessionManager,
1092
1171
  session,
1093
- agent,
1172
+ extensionSkills,
1094
1173
  setEventContext,
1095
1174
  setSandboxContext,
1096
1175
  setUploadFunction,
@@ -1119,20 +1198,33 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
1119
1198
  actorName: formatAgentActorName(message.userName, prepared.sessionConversation),
1120
1199
  event: { kind: "sessionStart" },
1121
1200
  });
1122
- await session.prompt(prepared.userMessage, prepared.imageAttachments.length > 0 ? { images: prepared.imageAttachments } : undefined);
1201
+ // Autonomous (event/trigger) runs get an explicit resource ceiling since
1202
+ // no human is watching the loop; interactive turns stay human-gated.
1203
+ const isEventRun = message.id.startsWith("event:");
1204
+ await session.prompt(prepared.userMessage, {
1205
+ ...(prepared.imageAttachments.length > 0 ? { images: prepared.imageAttachments } : {}),
1206
+ ...(isEventRun ? { budget: DEFAULT_EVENT_BUDGET } : {}),
1207
+ });
1123
1208
  // Wait for queued messages
1124
1209
  await prepared.runQueue.wait();
1125
1210
  const sessionViewTokenStore = sessionView?.tokenStore;
1126
1211
  const sessionViewPortalBaseUrl = sessionView?.portalBaseUrl;
1127
- const createOverflowLink = sessionViewTokenStore && sessionViewPortalBaseUrl
1212
+ let sessionViewLink;
1213
+ const createSessionViewLink = sessionViewTokenStore && sessionViewPortalBaseUrl
1128
1214
  ? () => {
1129
- const token = sessionViewTokenStore.create(platform.name, message.userId, conversationId, message.sessionKey, contextFile, message.userName);
1130
- return `${sessionViewPortalBaseUrl}/session?token=${token.token}`;
1215
+ if (!sessionViewLink) {
1216
+ const token = sessionViewTokenStore.create(platform.name, message.userId, conversationId, message.sessionKey, contextFile, message.userName);
1217
+ sessionViewLink = `${sessionViewPortalBaseUrl}/session?token=${token.token}`;
1218
+ }
1219
+ return sessionViewLink;
1131
1220
  }
1132
1221
  : undefined;
1133
1222
  await finalizeRunResponse(responder, session, runState, {
1134
1223
  triggerAttribution: prepared.triggerAttribution,
1135
- createOverflowLink,
1224
+ triggerSessionLink: isEventTriggerAttribution(prepared.triggerAttribution)
1225
+ ? createSessionViewLink?.()
1226
+ : undefined,
1227
+ createOverflowLink: createSessionViewLink,
1136
1228
  platform: platform.name,
1137
1229
  model,
1138
1230
  sessionConversation: prepared.sessionConversation,