@geminixiang/mikan 0.5.3 → 1.0.0-beta.1

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 (114) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +2 -2
  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/agent.d.ts +10 -1
  7. package/dist/agent.d.ts.map +1 -1
  8. package/dist/agent.js +153 -95
  9. package/dist/agent.js.map +1 -1
  10. package/dist/commands/model.d.ts +2 -2
  11. package/dist/commands/model.d.ts.map +1 -1
  12. package/dist/commands/model.js.map +1 -1
  13. package/dist/commands/registry.d.ts.map +1 -1
  14. package/dist/commands/registry.js +2 -5
  15. package/dist/commands/registry.js.map +1 -1
  16. package/dist/config.d.ts +33 -0
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/config.js +83 -5
  19. package/dist/config.js.map +1 -1
  20. package/dist/execution-resolver.d.ts.map +1 -1
  21. package/dist/execution-resolver.js +2 -3
  22. package/dist/execution-resolver.js.map +1 -1
  23. package/dist/harness/auth.d.ts +16 -0
  24. package/dist/harness/auth.d.ts.map +1 -0
  25. package/dist/harness/auth.js +86 -0
  26. package/dist/harness/auth.js.map +1 -0
  27. package/dist/harness/extensions/loader.d.ts +43 -0
  28. package/dist/harness/extensions/loader.d.ts.map +1 -0
  29. package/dist/harness/extensions/loader.js +325 -0
  30. package/dist/harness/extensions/loader.js.map +1 -0
  31. package/dist/harness/extensions/registry.d.ts +23 -0
  32. package/dist/harness/extensions/registry.d.ts.map +1 -0
  33. package/dist/harness/extensions/registry.js +45 -0
  34. package/dist/harness/extensions/registry.js.map +1 -0
  35. package/dist/harness/extensions/types.d.ts +225 -0
  36. package/dist/harness/extensions/types.d.ts.map +1 -0
  37. package/dist/harness/extensions/types.js +2 -0
  38. package/dist/harness/extensions/types.js.map +1 -0
  39. package/dist/harness/http.d.ts +16 -0
  40. package/dist/harness/http.d.ts.map +1 -0
  41. package/dist/harness/http.js +71 -0
  42. package/dist/harness/http.js.map +1 -0
  43. package/dist/harness/index.d.ts +19 -0
  44. package/dist/harness/index.d.ts.map +1 -0
  45. package/dist/harness/index.js +18 -0
  46. package/dist/harness/index.js.map +1 -0
  47. package/dist/harness/models.d.ts +34 -0
  48. package/dist/harness/models.d.ts.map +1 -0
  49. package/dist/harness/models.js +248 -0
  50. package/dist/harness/models.js.map +1 -0
  51. package/dist/harness/runner.d.ts +124 -0
  52. package/dist/harness/runner.d.ts.map +1 -0
  53. package/dist/harness/runner.js +436 -0
  54. package/dist/harness/runner.js.map +1 -0
  55. package/dist/harness/session-store.d.ts +77 -0
  56. package/dist/harness/session-store.d.ts.map +1 -0
  57. package/dist/harness/session-store.js +248 -0
  58. package/dist/harness/session-store.js.map +1 -0
  59. package/dist/harness/settings.d.ts +47 -0
  60. package/dist/harness/settings.d.ts.map +1 -0
  61. package/dist/harness/settings.js +29 -0
  62. package/dist/harness/settings.js.map +1 -0
  63. package/dist/harness/skills.d.ts +54 -0
  64. package/dist/harness/skills.d.ts.map +1 -0
  65. package/dist/harness/skills.js +203 -0
  66. package/dist/harness/skills.js.map +1 -0
  67. package/dist/harness/types.d.ts +32 -0
  68. package/dist/harness/types.d.ts.map +1 -0
  69. package/dist/harness/types.js +2 -0
  70. package/dist/harness/types.js.map +1 -0
  71. package/dist/index.d.ts +1 -0
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +1 -0
  74. package/dist/index.js.map +1 -1
  75. package/dist/main.js +65 -3
  76. package/dist/main.js.map +1 -1
  77. package/dist/model-registry.d.ts +2 -2
  78. package/dist/model-registry.d.ts.map +1 -1
  79. package/dist/model-registry.js.map +1 -1
  80. package/dist/runtime/conversation-runtime.js +1 -1
  81. package/dist/runtime/conversation-runtime.js.map +1 -1
  82. package/dist/runtime/types.d.ts +3 -1
  83. package/dist/runtime/types.d.ts.map +1 -1
  84. package/dist/runtime/types.js.map +1 -1
  85. package/dist/sessions/agent-memory-file-manager.d.ts.map +1 -1
  86. package/dist/sessions/agent-memory-file-manager.js +8 -8
  87. package/dist/sessions/agent-memory-file-manager.js.map +1 -1
  88. package/dist/sessions/metadata.js +2 -2
  89. package/dist/sessions/metadata.js.map +1 -1
  90. package/dist/sessions/store.d.ts +3 -3
  91. package/dist/sessions/store.d.ts.map +1 -1
  92. package/dist/sessions/store.js +4 -4
  93. package/dist/sessions/store.js.map +1 -1
  94. package/dist/sessions/types.d.ts +1 -1
  95. package/dist/sessions/types.d.ts.map +1 -1
  96. package/dist/sessions/types.js.map +1 -1
  97. package/dist/trigger.d.ts.map +1 -1
  98. package/dist/trigger.js +5 -13
  99. package/dist/trigger.js.map +1 -1
  100. package/dist/types.d.ts +6 -0
  101. package/dist/types.d.ts.map +1 -1
  102. package/dist/types.js.map +1 -1
  103. package/dist/vault/index.d.ts.map +1 -1
  104. package/dist/vault/index.js +7 -1
  105. package/dist/vault/index.js.map +1 -1
  106. package/dist/web/admin/portal.d.ts.map +1 -1
  107. package/dist/web/admin/portal.js +328 -9
  108. package/dist/web/admin/portal.js.map +1 -1
  109. package/dist/web/admin/provider-models.d.ts +2 -2
  110. package/dist/web/admin/provider-models.d.ts.map +1 -1
  111. package/dist/web/admin/provider-models.js.map +1 -1
  112. package/dist/web/session-view/service.js +5 -5
  113. package/dist/web/session-view/service.js.map +1 -1
  114. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -9,6 +9,33 @@ any release.
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [1.0.0-beta.1]
13
+
14
+ ### Fixed
15
+
16
+ - Accept `maxOutputTokens` as an alias for `maxTokens` in custom model configuration.
17
+ - Restore npm release automation with compatible Node.js and npm versions.
18
+
19
+ ### Tests
20
+
21
+ - Add coverage for production-shaped custom model configuration.
22
+
23
+ ## [1.0.0-beta.0]
24
+
25
+ ### Added
26
+
27
+ - Add mikan's native agent harness with model, authentication, session, compaction, and skill support.
28
+ - Add an extension system for hooks, schedules, notifications, data directories, secrets, and bundled skills, with an agent project manager example.
29
+ - Add per-conversation token usage bar charts to the admin portal.
30
+
31
+ ### Changed
32
+
33
+ - Replace the pi-coding-agent runtime and session integration with the native harness built on pi-agent-core and pi-ai.
34
+
35
+ ### Tests
36
+
37
+ - Add coverage for harness authentication, extensions, HTTP helpers, runner, sessions, and skills.
38
+
12
39
  ## [0.5.3] - 2026-06-30
13
40
 
14
41
  ### Added
package/README.md CHANGED
@@ -16,8 +16,8 @@ mikan keeps the chat record, agent session, and execution runtime separate:
16
16
  ![mikan architecture](src/content/docs/assets/architecture.png)
17
17
 
18
18
  - **Chat / conversation data** is the platform-facing record: `log.jsonl`, attachments, and conversation files.
19
- - **Session orchestration** turns platform events into agent runs, handles top-level/thread scopes, and persists pi-coding-agent structured context under `sessions/*.jsonl`.
20
- - **pi-coding-agent harness** runs the model loop and calls mikan tools.
19
+ - **Session orchestration** turns platform events into agent runs, handles top-level/thread scopes, and persists structured context under `sessions/*.jsonl`.
20
+ - **mikan agent harness** (`src/harness/`, built on pi-agent-core and pi-ai) runs the model loop, session persistence, compaction, and calls mikan tools.
21
21
  - **Sandbox runtime** is where tool commands execute: host, Docker container/image, Firecracker, or Cloudflare bridge.
22
22
  - **Vault** provides runtime credentials as env vars and mounted secret files.
23
23
 
package/dist/adapter.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export type { MessagingBot, ConversationContext, ConversationEvent, MessagingEventHandler, ChatAdapter, ConversationMessage, ConversationResponder, ChatToolResult, ConversationKind, MessagingInfo, PlatformName, RunningSession, } from "./types.js";
1
+ export type { MessagingBot, ConversationContext, ConversationEvent, MessagingEventHandler, ChatAdapter, ConversationMessage, ConversationResponder, ChatToolResult, ConversationKind, MessagingInfo, PlatformName, PlatformNotifier, RunningSession, } from "./types.js";
2
2
  //# sourceMappingURL=adapter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,YAAY,EACZ,mBAAmB,EACnB,iBAAiB,EACjB,qBAAqB,EACrB,WAAW,EACX,mBAAmB,EACnB,qBAAqB,EACrB,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,cAAc,GACf,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,YAAY,EACZ,mBAAmB,EACnB,iBAAiB,EACjB,qBAAqB,EACrB,WAAW,EACX,mBAAmB,EACnB,qBAAqB,EACrB,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,gBAAgB,EAChB,cAAc,GACf,MAAM,YAAY,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"","sourcesContent":["export type {\n MessagingBot,\n ConversationContext,\n ConversationEvent,\n MessagingEventHandler,\n ChatAdapter,\n ConversationMessage,\n ConversationResponder,\n ChatToolResult,\n ConversationKind,\n MessagingInfo,\n PlatformName,\n RunningSession,\n} from \"./types.js\";\n"]}
1
+ {"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"","sourcesContent":["export type {\n MessagingBot,\n ConversationContext,\n ConversationEvent,\n MessagingEventHandler,\n ChatAdapter,\n ConversationMessage,\n ConversationResponder,\n ChatToolResult,\n ConversationKind,\n MessagingInfo,\n PlatformName,\n PlatformNotifier,\n RunningSession,\n} from \"./types.js\";\n"]}
package/dist/agent.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type ImageContent } from "@earendil-works/pi-ai";
2
2
  import type { ConversationMessage } from "./adapter.js";
3
+ import type { PlatformNotifier } from "./types.js";
3
4
  import type { SessionViewTokenStoreLike } from "./commands/types.js";
4
5
  import type { DockerContainerManager } from "./provisioner.js";
5
6
  import { type RuntimePathContext, type SandboxConfig } from "./sandbox/index.js";
@@ -8,6 +9,14 @@ import { type ResolvedSessionScope } from "./sessions/store.js";
8
9
  export type { PiAgentWrapper } from "./types.js";
9
10
  import type { PiAgentWrapper } from "./types.js";
10
11
  export declare function resolveTriggerAttribution(message: Pick<ConversationMessage, "id" | "text" | "userName">): string | undefined;
12
+ /**
13
+ * Instructions that vary per turn (event-trigger mode, response attribution).
14
+ * These are delivered with the user message rather than baked into the system
15
+ * prompt, so the system prompt stays cache-stable across a conversation's turns
16
+ * (in a multi-user channel the attribution line alone changes every turn).
17
+ * Returns an empty string when the turn needs no special framing.
18
+ */
19
+ export declare function buildTurnInstructions(isEventTrigger: boolean, triggerAttribution: string | undefined, platformName: string): string;
11
20
  export declare function getUnresolvedSandboxPathContext(sandboxConfig: SandboxConfig, hostWorkspaceRoot: string): RuntimePathContext;
12
21
  export declare function buildPromptPayload(message: ConversationMessage, workspacePath: string, pathContext?: RuntimePathContext): {
13
22
  userMessage: string;
@@ -24,7 +33,7 @@ export declare function appendTriggerAttribution(text: string, triggerAttributio
24
33
  export declare function createRunner(sandboxConfig: SandboxConfig, sessionKey: string, conversationId: string, conversationDir: string, workspaceDir: string, sessionScope: ResolvedSessionScope, vaultManager?: VaultManager, provisioner?: DockerContainerManager, sessionView?: {
25
34
  tokenStore: SessionViewTokenStoreLike;
26
35
  portalBaseUrl?: string;
27
- }): Promise<PiAgentWrapper>;
36
+ }, platformNotifier?: PlatformNotifier): Promise<PiAgentWrapper>;
28
37
  export declare function translateRuntimePathToHost(runtimePath: string, pathContext: RuntimePathContext): string;
29
38
  export declare function translateAttachPathToHost(filePath: string, pathContext: RuntimePathContext): string;
30
39
  //# sourceMappingURL=agent.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,KAAK,YAAY,EAAc,MAAM,uBAAuB,CAAC;AAkBhF,OAAO,KAAK,EACV,mBAAmB,EAKpB,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAKrE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EAGL,KAAK,kBAAkB,EACvB,KAAK,aAAa,EACnB,MAAM,oBAAoB,CAAC;AAO5B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,OAAO,EAGL,KAAK,oBAAoB,EAE1B,MAAM,qBAAqB,CAAC;AAO7B,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAyIjD,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,IAAI,CAAC,mBAAmB,EAAE,IAAI,GAAG,MAAM,GAAG,UAAU,CAAC,GAC7D,MAAM,GAAG,SAAS,CAOpB;AA+LD,wBAAgB,+BAA+B,CAC7C,aAAa,EAAE,aAAa,EAC5B,iBAAiB,EAAE,MAAM,GACxB,kBAAkB,CAMpB;AA+SD,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,mBAAmB,EAC5B,aAAa,EAAE,MAAM,EACrB,WAAW,CAAC,EAAE,kBAAkB,GAC/B;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,YAAY,EAAE,CAAC;CAClC,CAaA;AA+BD,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,kBAAkB,EAAE,MAAM,GAAG,SAAS,EACtC,WAAW,CAAC,EAAE,MAAM,GACnB,MAAM,CAYR;AA+qBD;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,aAAa,EAAE,aAAa,EAC5B,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,MAAM,EACtB,eAAe,EAAE,MAAM,EACvB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,oBAAoB,EAClC,YAAY,CAAC,EAAE,YAAY,EAC3B,WAAW,CAAC,EAAE,sBAAsB,EACpC,WAAW,CAAC,EAAE;IACZ,UAAU,EAAE,yBAAyB,CAAC;IACtC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GACA,OAAO,CAAC,cAAc,CAAC,CA0NzB;AAED,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,kBAAkB,GAC9B,MAAM,CAER;AAED,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,kBAAkB,GAC9B,MAAM,CAKR"}
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,KAAK,YAAY,EAAc,MAAM,uBAAuB,CAAC;AAkBhF,OAAO,KAAK,EACV,mBAAmB,EAKpB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAiC,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAClF,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAMrE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EAGL,KAAK,kBAAkB,EACvB,KAAK,aAAa,EACnB,MAAM,oBAAoB,CAAC;AAO5B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,OAAO,EAGL,KAAK,oBAAoB,EAE1B,MAAM,qBAAqB,CAAC;AAQ7B,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAyIjD,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,IAAI,CAAC,mBAAmB,EAAE,IAAI,GAAG,MAAM,GAAG,UAAU,CAAC,GAC7D,MAAM,GAAG,SAAS,CAOpB;AA6KD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,cAAc,EAAE,OAAO,EACvB,kBAAkB,EAAE,MAAM,GAAG,SAAS,EACtC,YAAY,EAAE,MAAM,GACnB,MAAM,CAkBR;AAED,wBAAgB,+BAA+B,CAC7C,aAAa,EAAE,aAAa,EAC5B,iBAAiB,EAAE,MAAM,GACxB,kBAAkB,CAMpB;AAwUD,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,mBAAmB,EAC5B,aAAa,EAAE,MAAM,EACrB,WAAW,CAAC,EAAE,kBAAkB,GAC/B;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,YAAY,EAAE,CAAC;CAClC,CAaA;AA+BD,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,kBAAkB,EAAE,MAAM,GAAG,SAAS,EACtC,WAAW,CAAC,EAAE,MAAM,GACnB,MAAM,CAYR;AAmsBD;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,aAAa,EAAE,aAAa,EAC5B,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,MAAM,EACtB,eAAe,EAAE,MAAM,EACvB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,oBAAoB,EAClC,YAAY,CAAC,EAAE,YAAY,EAC3B,WAAW,CAAC,EAAE,sBAAsB,EACpC,WAAW,CAAC,EAAE;IACZ,UAAU,EAAE,yBAAyB,CAAC;IACtC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,EACD,gBAAgB,CAAC,EAAE,gBAAgB,GAClC,OAAO,CAAC,cAAc,CAAC,CAyNzB;AAED,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,kBAAkB,GAC9B,MAAM,CAER;AAED,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,kBAAkB,GAC9B,MAAM,CAKR"}
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");
@@ -358,58 +369,81 @@ function createRunnerExecutionContext(sandboxConfig, vaultManager, provisioner,
358
369
  },
359
370
  };
360
371
  }
361
- async function createConfiguredAgentSession(params) {
362
- const { conversationId, workspaceDir, runtimeWorkspaceRoot, systemPrompt, model, thinkingLevel, tools, sessionManager, settingsManager, modelRegistry, } = params;
363
- const agent = new Agent({
364
- initialState: {
365
- systemPrompt,
366
- model,
367
- thinkingLevel,
368
- 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
+ })),
369
394
  },
370
- convertToLlm,
371
- getApiKey: async (provider) => {
372
- const key = await modelRegistry.getApiKeyForProvider(provider);
373
- if (!key) {
374
- 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 ?? {},
375
399
  }
376
- return key;
377
- },
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 }),
378
426
  });
379
- const loadedSession = sessionManager.buildSessionContext();
380
- if (loadedSession.messages.length > 0) {
381
- agent.state.messages = loadedSession.messages;
382
- 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);
383
429
  }
384
- const resourceLoader = new DefaultResourceLoader({
385
- cwd: workspaceDir,
386
- 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({
387
434
  systemPrompt,
435
+ model,
436
+ thinkingLevel,
437
+ tools,
438
+ models,
439
+ sessionStore,
440
+ extensions: extensionsResult.registry,
388
441
  });
389
- try {
390
- await resourceLoader.reload();
391
- const extResult = resourceLoader.getExtensions();
392
- if (extResult.errors.length > 0) {
393
- for (const err of extResult.errors) {
394
- log.logWarning(`[${conversationId}] Extension load error: ${err.path}`, err.error);
395
- }
396
- }
397
- log.logInfo(`[${conversationId}] Loaded ${extResult.extensions.length} extension(s): ${extResult.extensions.map((extension) => extension.path).join(", ")}`);
398
- }
399
- catch (error) {
400
- 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`);
401
445
  }
402
- const baseToolsOverride = Object.fromEntries(tools.map((tool) => [tool.name, tool]));
403
- const session = new AgentSession({
404
- agent,
405
- sessionManager,
406
- settingsManager,
407
- cwd: runtimeWorkspaceRoot,
408
- modelRegistry,
409
- resourceLoader,
410
- baseToolsOverride,
411
- });
412
- return { agent, session };
446
+ return { session, extensionSkills: extensionsResult.skills };
413
447
  }
414
448
  function createEmptyUsageTotals() {
415
449
  return {
@@ -705,15 +739,14 @@ async function reportUsageSummary(ctx) {
705
739
  await waitForQueue();
706
740
  }
707
741
  }
708
- function reloadSessionMessages(sessionManager, conversationId, agent) {
709
- const messages = sessionManager.buildSessionContext().messages;
710
- if (messages.length > 0) {
711
- agent.state.messages = messages;
712
- 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`);
713
746
  }
714
747
  }
715
748
  async function prepareRunContext(params) {
716
- 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;
717
750
  let pathContext = params.pathContext;
718
751
  const sessionConversation = message.sessionKey.split(":")[0];
719
752
  await mkdir(join(conversationDir, "scratch"), { recursive: true });
@@ -725,12 +758,19 @@ async function prepareRunContext(params) {
725
758
  });
726
759
  pathContext = getPathContext();
727
760
  }
728
- reloadSessionMessages(sessionManager, conversationId, agent);
761
+ reloadSessionMessages(session, conversationId);
729
762
  const memory = await getMemory(conversationDir);
730
- const skills = loadMikanSkills(conversationDir, pathContext.runtimeWorkspaceRoot);
763
+ const skills = mergeExtensionSkills(loadMikanSkills(conversationDir, pathContext.runtimeWorkspaceRoot), params.extensionSkills ?? []);
731
764
  const triggerAttribution = resolveTriggerAttribution(message);
732
- 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);
733
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}`);
734
774
  setEventContext({
735
775
  platform: platform.name,
736
776
  conversationId,
@@ -748,11 +788,13 @@ async function prepareRunContext(params) {
748
788
  log.logInfo(`Context sizes - system: ${systemPrompt.length} chars, memory: ${memory.length} chars`);
749
789
  log.logInfo(`Channels: ${platform.channels.length}, Users: ${platform.users.length}`);
750
790
  const { userMessage, imageAttachments } = buildPromptPayload(message, pathContext.runtimeWorkspaceRoot, pathContext);
751
- 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);
752
794
  return {
753
795
  sessionConversation,
754
796
  runQueue,
755
- userMessage,
797
+ userMessage: finalUserMessage,
756
798
  imageAttachments,
757
799
  triggerAttribution,
758
800
  pathContext,
@@ -1015,6 +1057,16 @@ function attachSessionEventHandlers(params) {
1015
1057
  });
1016
1058
  queue.enqueue(() => responder.respond(text), "retry");
1017
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");
1069
+ }
1018
1070
  });
1019
1071
  }
1020
1072
  function extractToolResultText(result) {
@@ -1045,15 +1097,17 @@ function extractToolResultText(result) {
1045
1097
  * Runner caching is handled by the caller (channelStates in main.ts).
1046
1098
  * This is a stateless factory function.
1047
1099
  */
1048
- 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) {
1049
1101
  const agentConfig = resolveConversationSettings(conversationDir);
1050
1102
  const workspaceBase = join(conversationDir, "..");
1051
1103
  const { executionResolver, executor, getPathContext, resolveExecutorForRun } = createRunnerExecutionContext(sandboxConfig, vaultManager, provisioner, workspaceDir, workspaceBase);
1052
1104
  let pathContext = getUnresolvedSandboxPathContext(sandboxConfig, workspaceBase);
1053
1105
  // Create tools (per-runner, with per-runner upload function setter)
1054
1106
  const { tools, setUploadFunction, setEventContext, setSandboxContext } = createMikanTools(executor, workspaceDir, { sandbox: sandboxConfig, provisioner });
1055
- const authStorage = AuthStorage.create(join(homedir(), ".pi", "mikan", "auth.json"));
1056
- const modelRegistry = ModelRegistry.create(authStorage);
1107
+ const modelRegistry = MikanModels.create();
1108
+ if (modelRegistry.getError()) {
1109
+ log.logWarning("models.json load error", modelRegistry.getError());
1110
+ }
1057
1111
  const model = resolveConfiguredModel(modelRegistry, agentConfig.provider, agentConfig.model);
1058
1112
  // Initial system prompt (will be updated each run with fresh memory/channels/users/skills)
1059
1113
  const memory = await getMemory(conversationDir);
@@ -1069,26 +1123,25 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
1069
1123
  // use the conversation's current pointer; scoped sessions use fixed files.
1070
1124
  // Platform-specific scope behavior is resolved before runner creation.
1071
1125
  const isThread = sessionKey.includes(":");
1072
- const { sessionDir, contextFile, threadRootMessage } = sessionScope;
1073
- const sessionManager = openManagedSession(contextFile, sessionDir, pathContext.runtimeWorkspaceRoot);
1126
+ const { contextFile, threadRootMessage } = sessionScope;
1127
+ const sessionManager = openManagedSession(contextFile, pathContext.runtimeWorkspaceRoot);
1074
1128
  const threadSessionName = buildThreadSessionName(threadRootMessage);
1075
1129
  if (isThread && threadSessionName && sessionManager.getSessionName() !== threadSessionName) {
1076
1130
  sessionManager.appendSessionInfo(threadSessionName);
1077
1131
  }
1078
1132
  const sessionUuid = extractSessionUuid(contextFile);
1079
1133
  const chatSessionManager = new AgentMemoryFileManager();
1080
- const settingsManager = SettingsManager.inMemory();
1081
- const { agent, session } = await createConfiguredAgentSession({
1134
+ const { session, extensionSkills } = await createConfiguredAgentSession({
1082
1135
  conversationId,
1083
1136
  workspaceDir,
1084
- runtimeWorkspaceRoot: pathContext.runtimeWorkspaceRoot,
1085
1137
  systemPrompt,
1086
1138
  model,
1087
1139
  thinkingLevel: agentConfig.thinkingLevel,
1088
1140
  tools,
1089
- sessionManager,
1090
- settingsManager,
1091
- modelRegistry,
1141
+ sessionStore: sessionManager,
1142
+ models: modelRegistry,
1143
+ vaultManager,
1144
+ platformNotifier,
1092
1145
  });
1093
1146
  // Mutable per-run state - event handler references this
1094
1147
  const runState = createRunState();
@@ -1115,9 +1168,8 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
1115
1168
  executionResolver,
1116
1169
  resolveExecutorForRun,
1117
1170
  getPathContext,
1118
- sessionManager,
1119
1171
  session,
1120
- agent,
1172
+ extensionSkills,
1121
1173
  setEventContext,
1122
1174
  setSandboxContext,
1123
1175
  setUploadFunction,
@@ -1146,7 +1198,13 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
1146
1198
  actorName: formatAgentActorName(message.userName, prepared.sessionConversation),
1147
1199
  event: { kind: "sessionStart" },
1148
1200
  });
1149
- 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
+ });
1150
1208
  // Wait for queued messages
1151
1209
  await prepared.runQueue.wait();
1152
1210
  const sessionViewTokenStore = sessionView?.tokenStore;