@voybio/ace-swarm 2.4.0 → 2.4.2

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 (80) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +502 -56
  3. package/assets/.agents/ACE/agent-qa/instructions.md +11 -0
  4. package/assets/agent-state/MODULES/schemas/RUNTIME_TOOL_SPEC_REGISTRY.schema.json +43 -0
  5. package/assets/agent-state/runtime-tool-specs.json +70 -2
  6. package/assets/instructions/ACE_Coder.instructions.md +13 -0
  7. package/assets/instructions/ACE_UI.instructions.md +11 -0
  8. package/dist/ace-context.js +70 -11
  9. package/dist/ace-internal-tools.d.ts +3 -1
  10. package/dist/ace-internal-tools.js +10 -2
  11. package/dist/agent-runtime/role-adapters.d.ts +18 -1
  12. package/dist/agent-runtime/role-adapters.js +49 -5
  13. package/dist/astgrep-index.d.ts +48 -0
  14. package/dist/astgrep-index.js +126 -1
  15. package/dist/cli.js +487 -17
  16. package/dist/discovery-runtime-wrappers.d.ts +108 -0
  17. package/dist/discovery-runtime-wrappers.js +615 -0
  18. package/dist/helpers/bootstrap.js +1 -1
  19. package/dist/helpers/constants.d.ts +4 -2
  20. package/dist/helpers/constants.js +8 -0
  21. package/dist/helpers/path-utils.d.ts +8 -1
  22. package/dist/helpers/path-utils.js +27 -8
  23. package/dist/helpers/store-resolution.js +7 -3
  24. package/dist/hermes/bridge-protocol.d.ts +41 -0
  25. package/dist/hermes/bridge-protocol.js +70 -0
  26. package/dist/hermes/launch-profile.d.ts +19 -0
  27. package/dist/hermes/launch-profile.js +81 -0
  28. package/dist/hermes/session-manager.d.ts +42 -0
  29. package/dist/hermes/session-manager.js +187 -0
  30. package/dist/job-scheduler.js +30 -4
  31. package/dist/json-sanitizer.d.ts +16 -0
  32. package/dist/json-sanitizer.js +26 -0
  33. package/dist/local-model-policy.d.ts +27 -0
  34. package/dist/local-model-policy.js +84 -0
  35. package/dist/local-model-runtime.d.ts +17 -0
  36. package/dist/local-model-runtime.js +77 -20
  37. package/dist/model-bridge.d.ts +6 -1
  38. package/dist/model-bridge.js +338 -21
  39. package/dist/orchestrator-supervisor.d.ts +42 -0
  40. package/dist/orchestrator-supervisor.js +110 -3
  41. package/dist/plan-proposal.d.ts +115 -0
  42. package/dist/plan-proposal.js +1073 -0
  43. package/dist/runtime-executor.d.ts +6 -1
  44. package/dist/runtime-executor.js +72 -5
  45. package/dist/runtime-tool-specs.d.ts +19 -1
  46. package/dist/runtime-tool-specs.js +67 -26
  47. package/dist/schemas.js +30 -1
  48. package/dist/server.d.ts +3 -0
  49. package/dist/server.js +73 -4
  50. package/dist/shared.d.ts +1 -0
  51. package/dist/shared.js +2 -0
  52. package/dist/store/bootstrap-store.d.ts +1 -0
  53. package/dist/store/bootstrap-store.js +8 -2
  54. package/dist/store/materializers/vericify-projector.js +3 -0
  55. package/dist/store/repositories/local-model-runtime-repository.d.ts +13 -1
  56. package/dist/store/repositories/local-model-runtime-repository.js +4 -1
  57. package/dist/store/repositories/vericify-repository.d.ts +1 -1
  58. package/dist/tools-agent.d.ts +20 -0
  59. package/dist/tools-agent.js +544 -29
  60. package/dist/tools-discovery.js +135 -0
  61. package/dist/tools-files.js +768 -66
  62. package/dist/tools-framework.js +80 -61
  63. package/dist/tools.d.ts +4 -1
  64. package/dist/tools.js +35 -13
  65. package/dist/tui/chat.d.ts +8 -0
  66. package/dist/tui/chat.js +74 -0
  67. package/dist/tui/index.d.ts +7 -0
  68. package/dist/tui/index.js +45 -2
  69. package/dist/tui/layout.d.ts +1 -0
  70. package/dist/tui/layout.js +4 -1
  71. package/dist/tui/ollama.d.ts +8 -1
  72. package/dist/tui/ollama.js +53 -12
  73. package/dist/tui/openai-compatible.d.ts +13 -0
  74. package/dist/tui/openai-compatible.js +305 -5
  75. package/dist/tui/provider-discovery.d.ts +1 -0
  76. package/dist/tui/provider-discovery.js +50 -24
  77. package/dist/vericify-bridge.d.ts +4 -1
  78. package/dist/vericify-bridge.js +3 -0
  79. package/package.json +2 -1
  80. package/scripts/hermes_bridge_worker.py +136 -0
@@ -21,6 +21,7 @@ import { auditStoreAuthority, writeStoreAuthorityAuditReport, } from "./store/st
21
21
  import { PROVENANCE_CRITICAL_EVENT_TYPES, validateArtifactManifestPayload, validateProvenanceLogContent, validateTealConfigContent, } from "./schemas.js";
22
22
  import { readAceTaskContractAssessment } from "./ace-autonomy.js";
23
23
  import { listStoreKeysSync, readStoreBlobSync } from "./store/store-snapshot.js";
24
+ import { parseJsonLikeText } from "./json-sanitizer.js";
24
25
  function executionRoleForDomain(domain) {
25
26
  switch (domain) {
26
27
  case "venture":
@@ -75,6 +76,14 @@ function parseGateManifest(raw, sourceRef) {
75
76
  return undefined;
76
77
  }
77
78
  }
79
+ function parseJsonForValidation(raw) {
80
+ return parseJsonLikeText(raw);
81
+ }
82
+ function jsonValidationDetail(parsed, validDetail = "valid JSON") {
83
+ return parsed.sanitized.removed_control_bytes > 0
84
+ ? `control bytes removed before parse (${parsed.sanitized.removed_control_bytes}); source requires repair`
85
+ : validDetail;
86
+ }
78
87
  function readGateManifests(gatesDir) {
79
88
  const files = readdirSync(gatesDir).filter((f) => f.endsWith(".json"));
80
89
  const allGates = [];
@@ -737,7 +746,10 @@ export function registerFrameworkTools(server) {
737
746
  "",
738
747
  "## LLM Runtime Profile",
739
748
  `- provider: ${llm_provider}`,
740
- `- model: ${resolvedLlmModel ?? defaultModelForProvider(llm_provider)}`,
749
+ `- model: ${resolvedLlmModel ??
750
+ (llm_provider === "llama.cpp"
751
+ ? "(set via ace connect or ace doctor --scan)"
752
+ : defaultModelForProvider(llm_provider))}`,
741
753
  `- base_url: ${resolvedLlmBaseUrl ??
742
754
  (llm_provider === "ollama" || llm_provider === "llama.cpp"
743
755
  ? "discover via ace doctor --scan or set explicitly"
@@ -1010,58 +1022,60 @@ export function registerFrameworkTools(server) {
1010
1022
  // Check: handoff registry parseable
1011
1023
  const hrRaw = safeRead("agent-state/handoff-registry.json");
1012
1024
  const hrOk = !hrRaw.startsWith("[FILE NOT FOUND]");
1025
+ let hrParsed;
1013
1026
  if (hrOk) {
1014
- try {
1015
- JSON.parse(hrRaw);
1027
+ const parsed = parseJsonForValidation(hrRaw);
1028
+ if (parsed.ok) {
1029
+ hrParsed = parsed.value;
1016
1030
  checks.push({
1017
1031
  name: "handoff-registry:parse",
1018
- ok: true,
1019
- detail: "valid JSON",
1032
+ ok: parsed.sanitized.removed_control_bytes === 0,
1033
+ detail: jsonValidationDetail(parsed),
1020
1034
  });
1021
1035
  }
1022
- catch {
1036
+ else {
1023
1037
  checks.push({
1024
1038
  name: "handoff-registry:parse",
1025
1039
  ok: false,
1026
- detail: "CORRUPT JSON",
1040
+ detail: `agent-state/handoff-registry.json: ${parsed.error}`,
1027
1041
  });
1028
1042
  }
1029
1043
  }
1030
1044
  // Check: run-ledger parseable
1031
1045
  const rlRaw = safeRead("agent-state/run-ledger.json");
1032
1046
  if (!rlRaw.startsWith("[FILE NOT FOUND]")) {
1033
- try {
1034
- JSON.parse(rlRaw);
1047
+ const parsed = parseJsonForValidation(rlRaw);
1048
+ if (parsed.ok) {
1035
1049
  checks.push({
1036
1050
  name: "run-ledger:parse",
1037
- ok: true,
1038
- detail: "valid JSON",
1051
+ ok: parsed.sanitized.removed_control_bytes === 0,
1052
+ detail: jsonValidationDetail(parsed),
1039
1053
  });
1040
1054
  }
1041
- catch {
1055
+ else {
1042
1056
  checks.push({
1043
1057
  name: "run-ledger:parse",
1044
1058
  ok: false,
1045
- detail: "CORRUPT JSON",
1059
+ detail: `agent-state/run-ledger.json: ${parsed.error}`,
1046
1060
  });
1047
1061
  }
1048
1062
  }
1049
1063
  // Check: todo-state parseable
1050
1064
  const tsRaw = safeRead("agent-state/todo-state.json");
1051
1065
  if (!tsRaw.startsWith("[FILE NOT FOUND]")) {
1052
- try {
1053
- JSON.parse(tsRaw);
1066
+ const parsed = parseJsonForValidation(tsRaw);
1067
+ if (parsed.ok) {
1054
1068
  checks.push({
1055
1069
  name: "todo-state:parse",
1056
- ok: true,
1057
- detail: "valid JSON",
1070
+ ok: parsed.sanitized.removed_control_bytes === 0,
1071
+ detail: jsonValidationDetail(parsed),
1058
1072
  });
1059
1073
  }
1060
- catch {
1074
+ else {
1061
1075
  checks.push({
1062
1076
  name: "todo-state:parse",
1063
1077
  ok: false,
1064
- detail: "CORRUPT JSON",
1078
+ detail: `agent-state/todo-state.json: ${parsed.error}`,
1065
1079
  });
1066
1080
  }
1067
1081
  }
@@ -1081,19 +1095,19 @@ export function registerFrameworkTools(server) {
1081
1095
  });
1082
1096
  continue;
1083
1097
  }
1084
- try {
1085
- JSON.parse(raw);
1098
+ const parsed = parseJsonForValidation(raw);
1099
+ if (parsed.ok) {
1086
1100
  checks.push({
1087
1101
  name: check.name,
1088
- ok: true,
1089
- detail: "valid JSON",
1102
+ ok: parsed.sanitized.removed_control_bytes === 0,
1103
+ detail: jsonValidationDetail(parsed),
1090
1104
  });
1091
1105
  }
1092
- catch {
1106
+ else {
1093
1107
  checks.push({
1094
1108
  name: check.name,
1095
1109
  ok: false,
1096
- detail: "CORRUPT JSON",
1110
+ detail: `${check.rel}: ${parsed.error}`,
1097
1111
  });
1098
1112
  }
1099
1113
  }
@@ -1147,25 +1161,28 @@ export function registerFrameworkTools(server) {
1147
1161
  const artifactManifestRaw = safeRead("agent-state/ARTIFACT_MANIFEST.json");
1148
1162
  let artifactManifestEntries = [];
1149
1163
  if (!artifactManifestRaw.startsWith("[FILE NOT FOUND]")) {
1150
- try {
1151
- const parsed = JSON.parse(artifactManifestRaw);
1152
- const validation = validateArtifactManifestPayload(parsed);
1164
+ const parsed = parseJsonForValidation(artifactManifestRaw);
1165
+ if (parsed.ok) {
1166
+ const validation = validateArtifactManifestPayload(parsed.value);
1167
+ const controlBytesOk = parsed.sanitized.removed_control_bytes === 0;
1153
1168
  checks.push({
1154
1169
  name: "artifact-manifest:schema",
1155
- ok: validation.ok,
1156
- detail: validation.ok
1157
- ? `valid (${validation.schema})`
1158
- : validation.errors.join("; "),
1170
+ ok: validation.ok && controlBytesOk,
1171
+ detail: !controlBytesOk
1172
+ ? jsonValidationDetail(parsed)
1173
+ : validation.ok
1174
+ ? `valid (${validation.schema})`
1175
+ : validation.errors.join("; "),
1159
1176
  });
1160
1177
  if (validation.ok) {
1161
- artifactManifestEntries = getArtifactManifestEntries(parsed);
1178
+ artifactManifestEntries = getArtifactManifestEntries(parsed.value);
1162
1179
  }
1163
1180
  }
1164
- catch {
1181
+ else {
1165
1182
  checks.push({
1166
1183
  name: "artifact-manifest:parse",
1167
1184
  ok: false,
1168
- detail: "CORRUPT JSON",
1185
+ detail: `agent-state/ARTIFACT_MANIFEST.json: ${parsed.error}`,
1169
1186
  });
1170
1187
  }
1171
1188
  }
@@ -1203,8 +1220,16 @@ export function registerFrameworkTools(server) {
1203
1220
  // Check: module registry role coverage vs event emitters
1204
1221
  const registryRaw = safeRead("agent-state/MODULES/registry.json");
1205
1222
  if (!registryRaw.startsWith("[FILE NOT FOUND]")) {
1206
- try {
1207
- const registry = JSON.parse(registryRaw);
1223
+ const parsed = parseJsonForValidation(registryRaw);
1224
+ if (parsed.ok) {
1225
+ if (parsed.sanitized.removed_control_bytes > 0) {
1226
+ checks.push({
1227
+ name: "registry:parse",
1228
+ ok: false,
1229
+ detail: jsonValidationDetail(parsed),
1230
+ });
1231
+ }
1232
+ const registry = parsed.value;
1208
1233
  const registeredRoles = new Set(registry.roles ?? []);
1209
1234
  const expectedEmitters = [
1210
1235
  "capability-safety",
@@ -1223,11 +1248,11 @@ export function registerFrameworkTools(server) {
1223
1248
  });
1224
1249
  }
1225
1250
  }
1226
- catch {
1251
+ else {
1227
1252
  checks.push({
1228
1253
  name: "registry:parse",
1229
1254
  ok: false,
1230
- detail: "registry.json is corrupt",
1255
+ detail: `agent-state/MODULES/registry.json: ${parsed.error}`,
1231
1256
  });
1232
1257
  }
1233
1258
  }
@@ -1248,30 +1273,24 @@ export function registerFrameworkTools(server) {
1248
1273
  });
1249
1274
  }
1250
1275
  // Check: handoff consistency (open handoffs have source files)
1251
- if (hrOk) {
1252
- try {
1253
- const registry = JSON.parse(hrRaw);
1254
- const handoffs = Object.values(registry.handoffs ?? {});
1255
- let openWithoutFile = 0;
1256
- for (const h of handoffs) {
1257
- if ((h.status === "open" || h.status === "accepted") &&
1258
- h.source_file) {
1259
- const fileContent = safeRead(h.source_file);
1260
- if (fileContent.startsWith("[FILE NOT FOUND]"))
1261
- openWithoutFile++;
1262
- }
1276
+ if (hrOk && hrParsed) {
1277
+ const handoffs = Object.values(hrParsed.handoffs ?? {});
1278
+ let openWithoutFile = 0;
1279
+ for (const h of handoffs) {
1280
+ if ((h.status === "open" || h.status === "accepted") &&
1281
+ h.source_file) {
1282
+ const fileContent = safeRead(h.source_file);
1283
+ if (fileContent.startsWith("[FILE NOT FOUND]"))
1284
+ openWithoutFile++;
1263
1285
  }
1264
- checks.push({
1265
- name: "handoff:source-file-integrity",
1266
- ok: openWithoutFile === 0,
1267
- detail: openWithoutFile === 0
1268
- ? "all open handoffs have source files"
1269
- : `${openWithoutFile} open handoffs reference missing source files`,
1270
- });
1271
- }
1272
- catch {
1273
- /* already caught above */
1274
1286
  }
1287
+ checks.push({
1288
+ name: "handoff:source-file-integrity",
1289
+ ok: openWithoutFile === 0,
1290
+ detail: openWithoutFile === 0
1291
+ ? "all open handoffs have source files"
1292
+ : `${openWithoutFile} open handoffs reference missing source files`,
1293
+ });
1275
1294
  }
1276
1295
  // Check: event provenance gaps in recent events.
1277
1296
  // Scans for provenance-critical event types that are missing
package/dist/tools.d.ts CHANGED
@@ -5,5 +5,8 @@
5
5
  * This keeps the registration entry-point small and each domain testable.
6
6
  */
7
7
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
- export declare function registerTools(server: McpServer): void;
8
+ export interface RegisterToolsOptions {
9
+ toolAllowlist?: readonly string[];
10
+ }
11
+ export declare function registerTools(server: McpServer, options?: RegisterToolsOptions): void;
9
12
  //# sourceMappingURL=tools.d.ts.map
package/dist/tools.js CHANGED
@@ -16,18 +16,40 @@ import { registerMemoryTools } from "./tools-memory.js";
16
16
  import { registerDriftTools } from "./tools-drift.js";
17
17
  import { registerSchedulerTools } from "./tools-scheduler.js";
18
18
  import { registerSkillTools } from "./tools-skills.js";
19
- export function registerTools(server) {
20
- registerAgentTools(server);
21
- registerHandoffTools(server);
22
- registerTodoTools(server);
23
- registerDiscoveryTools(server);
24
- registerLifecycleTools(server);
25
- registerFileTools(server);
26
- registerFrameworkTools(server);
27
- registerGitTools(server);
28
- registerMemoryTools(server);
29
- registerDriftTools(server);
30
- registerSchedulerTools(server);
31
- registerSkillTools(server);
19
+ const TOOL_REGISTRARS = [
20
+ registerAgentTools,
21
+ registerHandoffTools,
22
+ registerTodoTools,
23
+ registerDiscoveryTools,
24
+ registerLifecycleTools,
25
+ registerFileTools,
26
+ registerFrameworkTools,
27
+ registerGitTools,
28
+ registerMemoryTools,
29
+ registerDriftTools,
30
+ registerSchedulerTools,
31
+ registerSkillTools,
32
+ ];
33
+ function createAllowlistedServer(server, allowed) {
34
+ return new Proxy(server, {
35
+ get(target, property, receiver) {
36
+ if (property !== "tool")
37
+ return Reflect.get(target, property, receiver);
38
+ const registerTool = Reflect.get(target, property, receiver);
39
+ return (name, ...args) => {
40
+ if (!allowed.has(name))
41
+ return undefined;
42
+ return Reflect.apply(registerTool, target, [name, ...args]);
43
+ };
44
+ },
45
+ });
46
+ }
47
+ export function registerTools(server, options = {}) {
48
+ const target = options.toolAllowlist === undefined
49
+ ? server
50
+ : createAllowlistedServer(server, new Set(options.toolAllowlist));
51
+ for (const register of TOOL_REGISTRARS) {
52
+ register(target);
53
+ }
32
54
  }
33
55
  //# sourceMappingURL=tools.js.map
@@ -12,6 +12,8 @@ import type { ChatMessage } from "./layout.js";
12
12
  import { ModelBridge } from "../model-bridge.js";
13
13
  import type { AceContextTier } from "../ace-context.js";
14
14
  import { type AceRuntimeStatusPacket } from "../store/repositories/local-model-runtime-repository.js";
15
+ import type { HermesLocalExecutor } from "../hermes/session-manager.js";
16
+ import type { HermesLaunchProfile } from "../hermes/launch-profile.js";
15
17
  export interface ChatSessionOptions {
16
18
  provider: string;
17
19
  model: string;
@@ -25,7 +27,10 @@ export interface ChatSessionOptions {
25
27
  aceRole?: string;
26
28
  aceTier?: AceContextTier;
27
29
  maxTurns?: number;
30
+ engine?: string;
28
31
  bridge?: Pick<ModelBridge, "run" | "interrupt">;
32
+ hermesExecutor?: HermesLocalExecutor;
33
+ hermesLaunchProfile?: HermesLaunchProfile;
29
34
  }
30
35
  export interface ChatSessionClients {
31
36
  ollama: Pick<OllamaClient, "chat" | "abort">;
@@ -51,7 +56,10 @@ export declare class ChatSession extends EventEmitter {
51
56
  private aceRole;
52
57
  private aceTier?;
53
58
  private maxTurns;
59
+ private executionEngine;
54
60
  private aceBridge;
61
+ private hermesExecutor?;
62
+ private hermesLaunchProfile?;
55
63
  private activeAceBridge;
56
64
  private providerBaseUrls;
57
65
  private sessionId;
package/dist/tui/chat.js CHANGED
@@ -13,6 +13,7 @@ import { ModelBridge } from "../model-bridge.js";
13
13
  import { resolveAceStateLayout } from "../ace-state-resolver.js";
14
14
  import { applyEvidenceGuardrail, buildAcePreflightPacket, buildBridgeTaskInput, buildContinuityRecord, buildStartupNudge, mapBridgeResultToRuntimeStatus, nextActivationLedger, } from "./local-model-contract.js";
15
15
  import { withLocalModelRuntimeRepository, } from "../store/repositories/local-model-runtime-repository.js";
16
+ import { parseExecutionEngine, runLocalModelTask } from "../local-model-runtime.js";
16
17
  export class ChatSession extends EventEmitter {
17
18
  clients;
18
19
  telemetry;
@@ -32,7 +33,10 @@ export class ChatSession extends EventEmitter {
32
33
  aceRole;
33
34
  aceTier;
34
35
  maxTurns;
36
+ executionEngine;
35
37
  aceBridge;
38
+ hermesExecutor;
39
+ hermesLaunchProfile;
36
40
  activeAceBridge = false;
37
41
  providerBaseUrls;
38
42
  sessionId = randomUUID();
@@ -56,7 +60,10 @@ export class ChatSession extends EventEmitter {
56
60
  this.aceRole = options.aceRole?.trim() || "orchestrator";
57
61
  this.aceTier = options.aceTier;
58
62
  this.maxTurns = options.maxTurns ?? 6;
63
+ this.executionEngine = parseExecutionEngine(options.engine) ?? "direct";
59
64
  this.aceBridge = options.bridge ?? new ModelBridge(clients);
65
+ this.hermesExecutor = options.hermesExecutor;
66
+ this.hermesLaunchProfile = options.hermesLaunchProfile;
60
67
  // Add system prompt if provided
61
68
  if (this.systemPrompt) {
62
69
  this.messages.push({ role: "system", content: this.systemPrompt });
@@ -349,6 +356,9 @@ export class ChatSession extends EventEmitter {
349
356
  recommended_next_action: input.recommendedNextAction,
350
357
  blocked_reason: input.blockedReason,
351
358
  updated_at: Date.now(),
359
+ execution_engine: this.executionEngine,
360
+ underlying_provider: this.provider,
361
+ underlying_model: this.model,
352
362
  };
353
363
  }
354
364
  setRuntimeStatus(status) {
@@ -536,6 +546,70 @@ export class ChatSession extends EventEmitter {
536
546
  this.activationLedger = activationLedger;
537
547
  return;
538
548
  }
549
+ if (this.executionEngine === "hermes_local") {
550
+ const delegated = await runLocalModelTask({
551
+ task: buildBridgeTaskInput(recentConversation, preflight),
552
+ role: executionRole,
553
+ workspaceRoot: this.workspaceRoot,
554
+ provider: streamProvider,
555
+ model: streamModel,
556
+ baseUrl: this.providerBaseUrls[streamProvider],
557
+ ollamaUrl: this.providerBaseUrls.ollama,
558
+ engine: "hermes_local",
559
+ maxTurns: this.maxTurns,
560
+ tier: this.aceTier ?? (executionRole === "orchestrator" ? "compressed" : "brief"),
561
+ hermesExecutor: this.hermesExecutor,
562
+ hermesLaunchProfile: this.hermesLaunchProfile,
563
+ });
564
+ const assistantText = delegated.result.summary.trim() || "Hermes-local turn completed.";
565
+ this.messages.push({ role: "assistant", content: assistantText });
566
+ this.displayMessages.push({
567
+ role: "assistant",
568
+ content: assistantText,
569
+ timestamp: Date.now(),
570
+ tokens: estimateTokenCount(assistantText),
571
+ });
572
+ runtimeStatus = {
573
+ ...runtimeStatus,
574
+ bridge_status: delegated.result.status === "completed"
575
+ ? "done"
576
+ : delegated.result.status === "failed"
577
+ ? "failed"
578
+ : delegated.result.status === "max_turns"
579
+ ? "needs_input"
580
+ : delegated.result.status,
581
+ blocked_reason: delegated.result.status === "failed" ? assistantText : undefined,
582
+ hermes_bridge_protocol_version: delegated.hermes?.bridge_protocol_version,
583
+ hermes_session_id: delegated.hermes?.hermes_session_id,
584
+ shadow_mcp_session_id: delegated.hermes?.shadow_mcp_session_id,
585
+ updated_at: Date.now(),
586
+ };
587
+ this.setRuntimeStatus(runtimeStatus);
588
+ const continuity = {
589
+ ...buildContinuityRecord({
590
+ preflight,
591
+ role: executionRole,
592
+ bridgeStatus: runtimeStatus.bridge_status,
593
+ evidenceRefs: delegated.result.evidence_refs ?? [],
594
+ }),
595
+ execution_engine: "hermes_local",
596
+ underlying_provider: streamProvider,
597
+ underlying_model: streamModel,
598
+ hermes_bridge_protocol_version: delegated.hermes?.bridge_protocol_version,
599
+ hermes_session_id: delegated.hermes?.hermes_session_id,
600
+ shadow_mcp_session_id: delegated.hermes?.shadow_mcp_session_id,
601
+ };
602
+ this.persistRuntimeSessionArtifacts({ activationLedger, runtimeStatus, continuity });
603
+ this.activationLedger = activationLedger;
604
+ this.telemetry.recordRequest({
605
+ startTime,
606
+ endTime: Date.now(),
607
+ promptTokens: estimateTokenCount(text),
608
+ completionTokens: estimateTokenCount(assistantText),
609
+ model: streamModel,
610
+ });
611
+ return;
612
+ }
539
613
  let bridgeOutput = "";
540
614
  const toolNames = [];
541
615
  let retryAttempt = 0;
@@ -5,6 +5,7 @@
5
5
  * This is the process entry when `ace tui` is invoked.
6
6
  */
7
7
  import { type TuiController } from "./commands.js";
8
+ import type { HermesLaunchProfile } from "../hermes/launch-profile.js";
8
9
  export declare class AceTui implements TuiController {
9
10
  private input;
10
11
  private layout;
@@ -18,6 +19,7 @@ export declare class AceTui implements TuiController {
18
19
  private chatSessions;
19
20
  private activeChatSession;
20
21
  private provider;
22
+ private executionEngine;
21
23
  private model;
22
24
  private ollamaAvailable;
23
25
  private providers;
@@ -32,6 +34,7 @@ export declare class AceTui implements TuiController {
32
34
  private providerBaseUrls;
33
35
  private latestRuntimeStatus;
34
36
  private pendingCloseTabId;
37
+ private hermesLaunchProfile?;
35
38
  constructor(options?: {
36
39
  provider?: string;
37
40
  model?: string;
@@ -40,6 +43,8 @@ export declare class AceTui implements TuiController {
40
43
  baseUrl?: string;
41
44
  ollamaUrl?: string;
42
45
  providerBaseUrls?: Record<string, string>;
46
+ engine?: string;
47
+ hermesLaunchProfile?: HermesLaunchProfile;
43
48
  workspaceRoot?: string;
44
49
  systemPrompt?: string;
45
50
  });
@@ -114,6 +119,8 @@ export declare function runTui(options?: {
114
119
  baseUrl?: string;
115
120
  ollamaUrl?: string;
116
121
  providerBaseUrls?: Record<string, string>;
122
+ engine?: string;
123
+ hermesLaunchProfile?: HermesLaunchProfile;
117
124
  workspaceRoot?: string;
118
125
  }): Promise<void>;
119
126
  //# sourceMappingURL=index.d.ts.map
package/dist/tui/index.js CHANGED
@@ -16,8 +16,9 @@ import { ChatSession } from "./chat.js";
16
16
  import { OpenAICompatibleClient, diagnoseChatRuntimeConfig, } from "./openai-compatible.js";
17
17
  import { detectColorLevel, write, cursor, screen, fg, style } from "./renderer.js";
18
18
  import { ALL_AGENTS, WORKSPACE_ROOT } from "../helpers.js";
19
+ import { parseExecutionEngine } from "../local-model-runtime.js";
19
20
  import { backfillHandoffsIntoScheduler } from "../tools-handoff.js";
20
- import { DEFAULT_OLLAMA_MODEL, defaultModelForProvider, inferProviderFromModel, normalizeLocalBaseUrl, } from "./provider-discovery.js";
21
+ import { DEFAULT_OLLAMA_MODEL, defaultModelForProvider, inferProviderFromModel, normalizeLocalBaseUrl, scanLocalModelRuntimes, } from "./provider-discovery.js";
21
22
  import { resolveAceStateLayout } from "../ace-state-resolver.js";
22
23
  import { withLocalModelRuntimeRepository, } from "../store/repositories/local-model-runtime-repository.js";
23
24
  const DASHBOARD_CONTROLS = ["provider", "model", "chat", "logs", "refresh"];
@@ -38,6 +39,7 @@ export class AceTui {
38
39
  activeChatSession = null;
39
40
  // State
40
41
  provider;
42
+ executionEngine;
41
43
  model;
42
44
  ollamaAvailable = false;
43
45
  providers = [];
@@ -52,11 +54,20 @@ export class AceTui {
52
54
  providerBaseUrls = new Map();
53
55
  latestRuntimeStatus = null;
54
56
  pendingCloseTabId = null;
57
+ hermesLaunchProfile;
55
58
  constructor(options = {}) {
56
59
  const workspaceRoot = options.workspaceRoot ?? WORKSPACE_ROOT;
57
60
  this.workspaceRoot = workspaceRoot;
61
+ this.hermesLaunchProfile = options.hermesLaunchProfile;
58
62
  this.provider = this.normalizeProvider(options.provider ?? inferProviderFromModel(options.model) ?? "ollama") ?? "ollama";
59
- this.model = (options.model ?? defaultModelForProvider(this.provider)).trim();
63
+ this.executionEngine = parseExecutionEngine(options.engine) ?? "direct";
64
+ const initialModel = options.model ??
65
+ (this.provider === "ollama"
66
+ ? defaultModelForProvider(this.provider)
67
+ : this.provider === "llama.cpp"
68
+ ? ""
69
+ : defaultModelForProvider(this.provider));
70
+ this.model = initialModel.trim();
60
71
  // Initialize modules
61
72
  const colorLevel = detectColorLevel();
62
73
  for (const [provider, baseUrl] of Object.entries(options.providerBaseUrls ?? {})) {
@@ -86,6 +97,7 @@ export class AceTui {
86
97
  this.config.set("ollama_url", this.providerBaseUrls.get("ollama"));
87
98
  }
88
99
  this.config.set("provider", this.provider);
100
+ this.config.set("engine", this.executionEngine);
89
101
  this.config.set("model", this.model);
90
102
  this.config.set("temperature", "0.7");
91
103
  this.config.set("top_p", "0.9");
@@ -142,6 +154,31 @@ export class AceTui {
142
154
  this.showMessage(`Provider '${this.provider}' loaded from session/config. Switch with /provider or dashboard controls.`, "info");
143
155
  this.checkProviderRuntimeConfig(this.provider, this.model, "startup");
144
156
  }
157
+ // Additionally probe local runtimes (llama.cpp / Ollama) and merge discoveries into TUI so users can explicitly pick either.
158
+ try {
159
+ const scan = await scanLocalModelRuntimes({ workspaceRoot: this.workspaceRoot, timeoutMs: 800 });
160
+ for (const c of scan.candidates) {
161
+ this.ensureProvider(c.provider);
162
+ this.setProviderBaseUrl(c.provider, c.baseUrl);
163
+ if (c.models && c.models.length > 0)
164
+ this.setProviderModels(c.provider, c.models, false);
165
+ }
166
+ // If the current model appears served by a discovered runtime and the provider wasn't explicitly forced, prefer the local runtime.
167
+ if (!process.env.ACE_TUI_PROVIDER) {
168
+ for (const c of scan.candidates) {
169
+ if (c.models.includes(this.model) && this.provider !== c.provider) {
170
+ this.provider = c.provider;
171
+ this.config.set("provider", this.provider);
172
+ this.telemetry.setModel(this.model);
173
+ this.showMessage(`Discovered local runtime '${c.provider}' serving model '${this.model}'. Provider set to '${this.provider}'.`, "info");
174
+ break;
175
+ }
176
+ }
177
+ }
178
+ }
179
+ catch {
180
+ // Ignore scan failures — discovery is best-effort.
181
+ }
145
182
  // Initial render
146
183
  this.fullRender();
147
184
  // Start 1-second tick for status bar updates
@@ -492,6 +529,7 @@ export class AceTui {
492
529
  const state = {
493
530
  taskState: this.getTaskState(),
494
531
  provider: this.provider,
532
+ engine: this.executionEngine,
495
533
  model: this.model,
496
534
  tokensIn: snap.tokensIn + agentTokens.input,
497
535
  tokensOut: snap.tokensOut + agentTokens.output,
@@ -757,6 +795,9 @@ export class AceTui {
757
795
  else if (this.provider === "ollama") {
758
796
  this.model = DEFAULT_OLLAMA_MODEL;
759
797
  }
798
+ else if (this.provider === "llama.cpp") {
799
+ this.model = "";
800
+ }
760
801
  else {
761
802
  this.model = defaultModelForProvider(this.provider);
762
803
  }
@@ -919,6 +960,7 @@ export class AceTui {
919
960
  // Create a chat session for this tab
920
961
  const session = new ChatSession({ ollama: this.ollama, openai: this.openai }, this.telemetry, {
921
962
  provider: this.provider,
963
+ engine: this.executionEngine,
922
964
  model: this.model,
923
965
  providerBaseUrls: Object.fromEntries(this.providerBaseUrls.entries()),
924
966
  systemPrompt: this.config.get("system_prompt"),
@@ -926,6 +968,7 @@ export class AceTui {
926
968
  topP: parseFloat(this.config.get("top_p") ?? "0.9"),
927
969
  numCtx: parseInt(this.config.get("num_ctx") ?? "8192", 10),
928
970
  workspaceRoot: this.workspaceRoot,
971
+ hermesLaunchProfile: this.hermesLaunchProfile,
929
972
  });
930
973
  // Wire chat session events
931
974
  session.on("updated", () => {
@@ -20,6 +20,7 @@ export interface LayoutZones {
20
20
  export interface StatusBarState {
21
21
  taskState: "idle" | "running" | "blocked" | "overdue";
22
22
  provider: string;
23
+ engine?: string;
23
24
  model: string;
24
25
  tokensIn: number;
25
26
  tokensOut: number;
@@ -160,6 +160,9 @@ export class LayoutManager {
160
160
  ? `${fg.gray}tab: ${fg.white}${state.activeTabLabel}${style.reset}`
161
161
  : undefined;
162
162
  const providerSeg = `${fg.cyan}provider: ${fg.brightCyan}${state.provider}${style.reset}`;
163
+ const engineSeg = state.engine
164
+ ? `${fg.cyan}engine: ${fg.brightCyan}${state.engine}${style.reset}`
165
+ : undefined;
163
166
  const modelSeg = `${fg.cyan}model: ${fg.brightCyan}${state.model}${style.reset}`;
164
167
  const runtimeSummary = formatRuntimeStatusSummary(state);
165
168
  const runtimeSeg = runtimeSummary
@@ -175,7 +178,7 @@ export class LayoutManager {
175
178
  : state.bridgeStatus
176
179
  ? `${fg.gray}${state.bridgeStatus}${style.reset}`
177
180
  : undefined;
178
- const segments = [stateIndicator, tabSeg, providerSeg, modelSeg, runtimeSeg, contextSeg, tokenSeg, timeSeg, clockSeg]
181
+ const segments = [stateIndicator, tabSeg, providerSeg, engineSeg, modelSeg, runtimeSeg, contextSeg, tokenSeg, timeSeg, clockSeg]
179
182
  .filter((segment) => Boolean(segment));
180
183
  const sep = ` ${fg.gray}│${style.reset} `;
181
184
  const line = ` ${segments.join(sep)} `;
@@ -111,6 +111,13 @@ export declare class OllamaClient {
111
111
  export declare class OllamaError extends Error {
112
112
  statusCode: number;
113
113
  responseBody: string;
114
- constructor(message: string, statusCode: number, responseBody: string);
114
+ kind?: string;
115
+ meta?: Record<string, unknown>;
116
+ suggested_remediation?: string;
117
+ constructor(message: string, statusCode: number, responseBody: string, options?: {
118
+ kind?: string;
119
+ model?: string;
120
+ suggested_remediation?: string;
121
+ });
115
122
  }
116
123
  //# sourceMappingURL=ollama.d.ts.map