@7n/tauri-components 0.10.1 → 0.11.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 (53) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/package.json +1 -1
  3. package/src/components/AgentDialog.vue +21 -46
  4. package/src/components/AuditDialog.vue +9 -17
  5. package/src/components/BaseDialog.vue +2 -6
  6. package/src/components/DialogActions.vue +2 -3
  7. package/src/components/RequestView.vue +2 -3
  8. package/src/components/StatePill.vue +1 -1
  9. package/src/components/docs/AgentDialog.md +4 -4
  10. package/src/components/docs/index.md +4 -4
  11. package/src/components/index.js +2 -2
  12. package/src/components/status.js +1 -1
  13. package/src/core/acp-agent-presets.js +2 -2
  14. package/src/core/acp-agent.js +21 -15
  15. package/src/core/acp-kit.js +79 -30
  16. package/src/core/dispatch.js +3 -3
  17. package/src/core/docs/acp-agent.md +4 -4
  18. package/src/core/docs/acp-kit.md +4 -4
  19. package/src/core/manifest.js +10 -8
  20. package/src/docs/index.md +4 -4
  21. package/src/index.js +3 -8
  22. package/src/testing/quasar.js +2 -2
  23. package/src/vue/docs/index.md +4 -4
  24. package/src/vue/docs/journal-store-tauri.md +4 -4
  25. package/src/vue/docs/use-acp-agent.md +4 -4
  26. package/src/vue/index.js +4 -6
  27. package/src/vue/journal-store-tauri.js +2 -2
  28. package/src/vue/use-acp-agent.js +22 -14
  29. package/src/vue/use-updater.js +4 -6
  30. package/types/core/dispatch.d.ts +5 -2
  31. package/types/core/manifest.d.ts +14 -9
  32. package/types/core/scope.d.ts +24 -11
  33. package/types/core/tools.d.ts +1 -1
  34. package/types/index.d.ts +4 -9
  35. package/src/core/agent-handler.js +0 -171
  36. package/src/core/agent-kit.js +0 -43
  37. package/src/core/docs/agent-handler.md +0 -31
  38. package/src/core/docs/agent-kit.md +0 -25
  39. package/src/core/docs/llm.md +0 -29
  40. package/src/core/docs/omlx-models.md +0 -30
  41. package/src/core/docs/resolve-omlx-base-url.md +0 -40
  42. package/src/core/llm.js +0 -90
  43. package/src/core/omlx-models.js +0 -34
  44. package/src/core/resolve-omlx-base-url.js +0 -84
  45. package/src/vue/docs/use-agent.md +0 -29
  46. package/src/vue/docs/use-omlx.md +0 -30
  47. package/src/vue/use-agent.js +0 -60
  48. package/src/vue/use-omlx.js +0 -102
  49. package/types/core/agent-handler.d.ts +0 -43
  50. package/types/core/agent-kit.d.ts +0 -30
  51. package/types/core/llm.d.ts +0 -52
  52. package/types/core/omlx-models.d.ts +0 -17
  53. package/types/core/resolve-omlx-base-url.d.ts +0 -42
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.11.0] - 2026-07-17
4
+
5
+ ### Removed
6
+
7
+ - useAgent()/createAgentKit (omlx/runAgent chat-completion шлях) видалено — `useAcpAgent()`/`createAcpAgentKit` (ACP: спавн codex/claude/cursor/pi) тепер єдиний агентний шлях. Разом з ним прибрано `core/llm.js`, `core/agent-handler.js`, `core/omlx-models.js`, `core/resolve-omlx-base-url.js`, `vue/use-omlx.js` та Rust-команду `omlx_config` (tauri-plugin-agent)
8
+
3
9
  ## [0.10.1] - 2026-07-16
4
10
 
5
11
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/tauri-components",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "description": "Shared LLM agent engine + Vue/Quasar UI for Tauri apps (chat, journal, trust-tier approval).",
6
6
  "license": "MIT",
@@ -3,33 +3,26 @@
3
3
  @update:model-value="val => emit('update:modelValue', val)"
4
4
  @show="onShow"
5
5
  :model-value="modelValue"
6
- title="Agent (local LLM)"
6
+ title="Agent"
7
7
  icon="sym_o_smart_toy"
8
8
  :width="560"
9
- body-class="q-gutter-sm"
10
- >
9
+ body-class="q-gutter-sm">
11
10
  <template #header>
12
- <q-btn @click="showConfig = !showConfig" icon="sym_o_tune" flat round dense size="sm" title="omlx config" />
11
+ <q-btn @click="showConfig = !showConfig" icon="sym_o_tune" flat round dense size="sm" title="agent config" />
13
12
  </template>
14
13
 
15
14
  <template v-if="showConfig">
16
- <q-input v-model="baseUrl" dense outlined label="omlx base URL" />
15
+ <q-select v-model="agentKind" :options="availableAgentKinds" dense outlined label="agent" />
17
16
  <q-select
18
- v-model="model"
19
- :options="models"
20
- @filter="(_, update) => update()"
21
- :loading="modelsLoading"
22
- use-input
23
- fill-input
24
- hide-selected
25
- input-debounce="0"
26
- new-value-mode="add-unique"
17
+ v-model="modelTier"
18
+ :options="availableTiers"
19
+ option-value="id"
20
+ option-label="label"
21
+ emit-value
22
+ map-options
27
23
  dense
28
24
  outlined
29
- label="model"
30
- hint="завантажені моделі omlx; можна вписати свою"
31
- />
32
- <q-input v-model="apiKey" dense outlined label="API key" type="password" />
25
+ label="tier" />
33
26
  <q-separator class="q-my-sm" />
34
27
  </template>
35
28
 
@@ -38,9 +31,7 @@
38
31
  <div v-if="turn.role === 'user'" class="chat-user">{{ turn.text }}</div>
39
32
  <RequestView v-else :result="turn.result" />
40
33
  </template>
41
- <div v-if="running" class="chat-thinking">
42
- <q-spinner-dots size="18px" /> думаю…
43
- </div>
34
+ <div v-if="running" class="chat-thinking"><q-spinner-dots size="18px" /> думаю…</div>
44
35
  </div>
45
36
 
46
37
  <q-input
@@ -52,8 +43,7 @@
52
43
  type="textarea"
53
44
  autogrow
54
45
  :label="inputLabel"
55
- :hint="promptHint"
56
- />
46
+ :hint="promptHint" />
57
47
 
58
48
  <!-- jscpd:ignore-start — formal footer markup; jscpd html-mode aligns its
59
49
  attribute tokens with an unrelated import block (no shared logic) -->
@@ -64,8 +54,7 @@
64
54
  :submit-label="sendLabel"
65
55
  icon="sym_o_play_arrow"
66
56
  :disable="sendDisabled"
67
- :loading="running"
68
- />
57
+ :loading="running" />
69
58
  </template>
70
59
  <!-- jscpd:ignore-end -->
71
60
  </BaseDialog>
@@ -78,17 +67,17 @@ import BaseDialog from './BaseDialog.vue'
78
67
  import DialogActions from './DialogActions.vue'
79
68
  import RequestView from './RequestView.vue'
80
69
 
81
- // The agent gateway is injected by the host app (`useAgent({ catalog, … })` from
82
- // @7n/tauri-components/vue), so this dialog stays domain-free and testable.
70
+ // The agent gateway is injected by the host app (`useAcpAgent({ catalog, … })`
71
+ // from @7n/tauri-components/vue), so this dialog stays domain-free and testable.
83
72
  const props = defineProps({
84
73
  modelValue: { type: Boolean, default: false },
85
74
  agent: { type: Object, required: true },
86
- promptHint: { type: String, default: 'наприклад: Create a task named deploy in /Users/.../mt, agent mode' },
75
+ promptHint: { type: String, default: 'наприклад: Create a task named deploy in /Users/.../mt, agent mode' }
87
76
  })
88
77
  const emit = defineEmits(['update:modelValue', 'ran'])
89
78
 
90
79
  const $q = useQuasar()
91
- const { baseUrl, model, apiKey, saveOmlx, loadOmlxEnv, listModels, request, respond } = props.agent
80
+ const { agentKind, modelTier, availableAgentKinds, availableTiers, loadEnv, request, respond } = props.agent
92
81
 
93
82
  const prompt = ref('')
94
83
  const running = ref(false)
@@ -96,8 +85,6 @@ const turns = ref([])
96
85
  const requestId = ref(null)
97
86
  const logEl = ref(null)
98
87
  const showConfig = ref(false)
99
- const models = ref([])
100
- const modelsLoading = ref(false)
101
88
 
102
89
  // Labels shift once a conversation is under way (fresh request → follow-up).
103
90
  const inputLabel = computed(() => (turns.value.length ? 'Повідомлення' : 'Prompt'))
@@ -105,23 +92,13 @@ const sendLabel = computed(() => (turns.value.length ? 'Надіслати' : '
105
92
  const sendDisabled = computed(() => running.value || !prompt.value.trim())
106
93
 
107
94
  /**
108
- * Pull omlx config from the user's global settings; reset the conversation.
95
+ * Resolve the per-machine default agent + start the domain MCP bridge; reset the conversation.
109
96
  */
110
97
  async function onShow() {
111
98
  prompt.value = ''
112
99
  turns.value = []
113
100
  requestId.value = null
114
- await loadOmlxEnv()
115
- // Populate the model dropdown from the omlx server (best-effort; stays editable).
116
- if (listModels) {
117
- modelsLoading.value = true
118
- try {
119
- models.value = await listModels()
120
- }
121
- finally {
122
- modelsLoading.value = false
123
- }
124
- }
101
+ await loadEnv()
125
102
  }
126
103
 
127
104
  /**
@@ -154,12 +131,10 @@ async function send() {
154
131
  scrollToEnd()
155
132
  running.value = true
156
133
  try {
157
- saveOmlx()
158
134
  apply(await (requestId.value ? respond(requestId.value, text) : request(text)))
159
135
  } catch (error) {
160
136
  $q.notify({ type: 'negative', message: String(error?.message ?? error) })
161
- }
162
- finally {
137
+ } finally {
163
138
  running.value = false
164
139
  }
165
140
  }
@@ -6,8 +6,7 @@
6
6
  title="Request journal"
7
7
  icon="sym_o_history"
8
8
  :width="680"
9
- body-class=""
10
- >
9
+ body-class="">
11
10
  <template #header>
12
11
  <q-btn @click="refresh" icon="sym_o_refresh" flat round dense size="sm" :loading="loading" title="Refresh" />
13
12
  </template>
@@ -36,8 +35,7 @@
36
35
  color="negative"
37
36
  unelevated
38
37
  no-caps
39
- :loading="busyId === rec.id"
40
- />
38
+ :loading="busyId === rec.id" />
41
39
  <q-btn @click="onApprove(rec, false)" label="Відхилити" flat no-caps :disable="busyId === rec.id" />
42
40
  </div>
43
41
  </div>
@@ -57,7 +55,7 @@ import StatePill from './StatePill.vue'
57
55
  // keeping this dialog domain-free.
58
56
  const props = defineProps({
59
57
  modelValue: { type: Boolean, default: false },
60
- agent: { type: Object, required: true },
58
+ agent: { type: Object, required: true }
61
59
  })
62
60
  const emit = defineEmits(['update:modelValue', 'changed'])
63
61
 
@@ -84,11 +82,9 @@ async function refresh() {
84
82
  loading.value = true
85
83
  try {
86
84
  records.value = await journal.list()
87
- }
88
- catch (error) {
85
+ } catch (error) {
89
86
  $q.notify({ type: 'negative', message: String(error?.message ?? error) })
90
- }
91
- finally {
87
+ } finally {
92
88
  loading.value = false
93
89
  }
94
90
  }
@@ -111,11 +107,9 @@ async function onRespond(rec, message) {
111
107
  await respond(rec.id, message)
112
108
  await refresh()
113
109
  emit('changed')
114
- }
115
- catch (error) {
110
+ } catch (error) {
116
111
  $q.notify({ type: 'negative', message: String(error?.message ?? error) })
117
- }
118
- finally {
112
+ } finally {
119
113
  busyId.value = null
120
114
  }
121
115
  }
@@ -131,11 +125,9 @@ async function onApprove(rec, ok) {
131
125
  await approve(rec.id, ok)
132
126
  await refresh()
133
127
  emit('changed')
134
- }
135
- catch (error) {
128
+ } catch (error) {
136
129
  $q.notify({ type: 'negative', message: String(error?.message ?? error) })
137
- }
138
- finally {
130
+ } finally {
139
131
  busyId.value = null
140
132
  }
141
133
  }
@@ -1,9 +1,5 @@
1
1
  <template>
2
- <q-dialog
3
- @update:model-value="value => emit('update:modelValue', value)"
4
- v-bind="$attrs"
5
- :model-value="modelValue"
6
- >
2
+ <q-dialog @update:model-value="value => emit('update:modelValue', value)" v-bind="$attrs" :model-value="modelValue">
7
3
  <q-card class="base-dialog" :style="{ width: `${width}px` }">
8
4
  <q-card-section class="row items-center no-wrap q-pb-sm">
9
5
  <q-icon v-if="icon" :name="icon" size="20px" class="q-mr-sm" />
@@ -40,7 +36,7 @@ defineProps({
40
36
  title: { type: String, required: true },
41
37
  icon: { type: String, default: '' },
42
38
  width: { type: Number, default: 520 },
43
- bodyClass: { type: String, default: 'q-gutter-md' },
39
+ bodyClass: { type: String, default: 'q-gutter-md' }
44
40
  })
45
41
  const emit = defineEmits(['update:modelValue'])
46
42
  </script>
@@ -8,8 +8,7 @@
8
8
  :loading="loading"
9
9
  color="primary"
10
10
  unelevated
11
- no-caps
12
- />
11
+ no-caps />
13
12
  </template>
14
13
 
15
14
  <script setup>
@@ -20,7 +19,7 @@ defineProps({
20
19
  submitLabel: { type: String, required: true },
21
20
  icon: { type: String, default: undefined },
22
21
  disable: { type: Boolean, default: false },
23
- loading: { type: Boolean, default: false },
22
+ loading: { type: Boolean, default: false }
24
23
  })
25
24
  const emit = defineEmits(['submit'])
26
25
  </script>
@@ -11,8 +11,7 @@
11
11
  <q-icon
12
12
  :name="action.envelope?.ok ? 'sym_o_check_circle' : 'sym_o_error'"
13
13
  :color="action.envelope?.ok ? 'positive' : 'negative'"
14
- size="14px"
15
- />
14
+ size="14px" />
16
15
  <code>{{ action.tool }}({{ JSON.stringify(action.input) }})</code>
17
16
  </div>
18
17
  </div>
@@ -23,7 +22,7 @@
23
22
  import StatePill from './StatePill.vue'
24
23
 
25
24
  defineProps({
26
- result: { type: Object, required: true },
25
+ result: { type: Object, required: true }
27
26
  })
28
27
  </script>
29
28
 
@@ -11,7 +11,7 @@
11
11
  import { statusColor } from './status.js'
12
12
 
13
13
  defineProps({
14
- status: { type: String, required: true },
14
+ status: { type: String, required: true }
15
15
  })
16
16
  </script>
17
17
 
@@ -1,12 +1,12 @@
1
1
  ---
2
+ type: Vue Component
3
+ title: AgentDialog.vue
4
+ resource: npm/src/components/AgentDialog.vue
2
5
  docgen:
3
- source: npm/src/components/AgentDialog.vue
4
- crc: 197cc9c0
6
+ crc: aa14e51b
5
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
6
8
  ---
7
9
 
8
- # AgentDialog.vue
9
-
10
10
  Компонент відображає діалогове вікно для взаємодії з локальною мовною моделлю (LLM) через агент. Він дозволяє користувачеві вводити запити, налаштовувати конфігурацію агента та бачити історію чату.
11
11
 
12
12
  ## Поведінка
@@ -1,14 +1,14 @@
1
1
  ---
2
+ type: JS Module
3
+ title: index.js
4
+ resource: npm/src/components/index.js
2
5
  docgen:
3
- source: npm/src/components/index.js
4
- crc: 42b50c52
6
+ crc: 6d136e3a
5
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
6
8
  score: 100
7
9
  judgeModel: openai-codex/gpt-5.4-mini
8
10
  ---
9
11
 
10
- # index.js
11
-
12
12
  ## Огляд
13
13
 
14
14
  Компоненти цього файлу керують відображенням діалогових вікон, які взаємодіють з агентом. Експортується базовий клас для діалогів, компоненти для відображення специфічних дій, деталей запиту та поточного стану, а також константи для визначення кольорів стану.
@@ -1,6 +1,6 @@
1
1
  // `@7n/tauri-components/components` — Quasar dialogs for the agent. AgentDialog
2
- // and AuditDialog take the injected `agent` gateway (from ./vue useAgent) via a
3
- // prop; the rest are pure props-only building blocks. Requires the host app to
2
+ // and AuditDialog take the injected `agent` gateway (from ./vue useAcpAgent) via
3
+ // a prop; the rest are pure props-only building blocks. Requires the host app to
4
4
  // provide vue + quasar (peer deps) and to have Quasar installed.
5
5
 
6
6
  export { default as AgentDialog } from './AgentDialog.vue'
@@ -9,7 +9,7 @@ export const STATUS_COLOR = {
9
9
  needs_clarification: '#64d2ff',
10
10
  needs_approval: '#ff9f0a',
11
11
  failed: '#ff453a',
12
- rejected: '#8e8e93',
12
+ rejected: '#8e8e93'
13
13
  }
14
14
 
15
15
  const FALLBACK = '#8e8e93'
@@ -20,6 +20,6 @@ export const CODEX_ACP_AGENT_PRESET = {
20
20
  tiers: {
21
21
  MIN: { label: 'GPT-5.6 Luna', env: codexModelEnv('gpt-5.6-luna') },
22
22
  AVG: { label: 'GPT-5.6 Terra', env: codexModelEnv('gpt-5.6-terra') },
23
- MAX: { label: 'GPT-5.6 Sol', env: codexModelEnv('gpt-5.6-sol') },
24
- },
23
+ MAX: { label: 'GPT-5.6 Sol', env: codexModelEnv('gpt-5.6-sol') }
24
+ }
25
25
  }
@@ -1,13 +1,11 @@
1
1
  import { invoke } from '@tauri-apps/api/core'
2
2
  import { listen } from '@tauri-apps/api/event'
3
3
 
4
- // ACP session driver: the replacement for llm.js's runAgent()/createOpenAiChat.
5
- // Instead of running our own chat-completion loop, we spawn an external ACP
6
- // agent (codex-acp/claude-agent-acp/cursor `agent acp`/pi-acp) via the Rust
7
- // plugin's acp_* commands and stream its `session/update` notifications back
8
- // into the same {content, trace, messages, stopped?} shape runAgent() used to
9
- // return, so agent-handler.js's runAndJournal needs no structural changes —
10
- // only which turn-runner it calls.
4
+ // ACP session driver: instead of running our own chat-completion loop, we
5
+ // spawn an external ACP agent (codex-acp/claude-agent-acp/cursor `agent acp`/
6
+ // pi-acp) via the Rust plugin's acp_* commands and stream its `session/update`
7
+ // notifications back into a {content, trace, messages, stopped?} shape that
8
+ // acp-kit.js's runAndJournal folds straight into the journal.
11
9
  //
12
10
  // Domain-tool approval (the MCP bridge's `acp://mcp-tool-call`) and native ACP
13
11
  // permission requests (`acp://permission-request`) are separate event streams
@@ -30,9 +28,18 @@ import { listen } from '@tauri-apps/api/event'
30
28
  * @param {boolean} [params.allowTerminal] grant terminal/*
31
29
  * @returns {Promise<{sessionKey: string, agentKind: string}>} session handle
32
30
  */
33
- export async function createAcpSession({ agentKind, command, args = [], env = {}, cwd, mcpBridgeUrl, allowFs = false, allowTerminal = false }) {
31
+ export async function createAcpSession({
32
+ agentKind,
33
+ command,
34
+ args = [],
35
+ env = {},
36
+ cwd,
37
+ mcpBridgeUrl,
38
+ allowFs = false,
39
+ allowTerminal = false
40
+ }) {
34
41
  const sessionKey = await invoke('plugin:agent|acp_spawn_agent', {
35
- args: { command, args, env, cwd, mcpBridgeUrl, allowFs, allowTerminal },
42
+ args: { command, args, env, cwd, mcpBridgeUrl, allowFs, allowTerminal }
36
43
  })
37
44
  return { sessionKey, agentKind }
38
45
  }
@@ -70,7 +77,7 @@ function applySessionUpdate(state, update) {
70
77
  state.calls.set(update.toolCallId, {
71
78
  tool: update.title ?? update.toolCallId,
72
79
  input: update.rawInput ?? {},
73
- envelope: null,
80
+ envelope: null
74
81
  })
75
82
  break
76
83
  }
@@ -100,7 +107,7 @@ function applySessionUpdate(state, update) {
100
107
  export async function runAcpTurn({ sessionKey, text, onChunk }) {
101
108
  const state = { text: '', messageId: null, calls: new Map() }
102
109
 
103
- const unlisten = await listen('acp://session-update', (event) => {
110
+ const unlisten = await listen('acp://session-update', event => {
104
111
  if (event.payload?.sessionKey !== sessionKey) return
105
112
  applySessionUpdate(state, event.payload.update)
106
113
  onChunk?.(event.payload.update)
@@ -115,10 +122,9 @@ export async function runAcpTurn({ sessionKey, text, onChunk }) {
115
122
  steps: 1,
116
123
  trace,
117
124
  messages,
118
- stopped: stoppedFromStopReason(stopReason),
125
+ stopped: stoppedFromStopReason(stopReason)
119
126
  }
120
- }
121
- finally {
127
+ } finally {
122
128
  unlisten()
123
129
  }
124
130
  }
@@ -175,7 +181,7 @@ export function respondAcpPermission(requestId, optionId) {
175
181
 
176
182
  /**
177
183
  * Register this app's tool catalog with the domain MCP bridge and start it.
178
- * @param {object[]} catalog tool definitions (same shape passed to `createAgentKit`)
184
+ * @param {object[]} catalog tool definitions (same shape passed to `createAcpAgentKit`)
179
185
  * @returns {Promise<string>} the bridge's loopback URL, e.g. `http://127.0.0.1:54321/`
180
186
  */
181
187
  export async function startAcpMcpBridge(catalog) {
@@ -3,17 +3,15 @@ import { createDispatch } from './dispatch.js'
3
3
  import { classify, DEFAULT_ACTOR_TIERS } from './scope.js'
4
4
 
5
5
  // createAcpAgentKit binds an app's catalog + journal to the ACP execution
6
- // engine — the ACP-flavored sibling of createAgentKit (agent-kit.js), added
7
- // alongside it so existing consumers keep working on the old omlx/runAgent
8
- // path while migrating at their own pace (see SPEC / plan for the sequencing).
6
+ // engine — the sole agent kit (the earlier omlx/runAgent-based createAgentKit
7
+ // has been removed; see CHANGELOG).
9
8
  //
10
- // The two engines differ in where tool dispatch happens: runAgent() drove its
11
- // own tool-calling loop and dispatched synchronously inside handleRequest.
12
- // Here the ACP agent process drives its OWN loop and calls domain tools
13
- // through the Rust-side MCP bridge — so dispatch is triggered by a STANDING
14
- // listener on `acp://mcp-tool-call`, not by anything in request()/respond().
15
- // Every such call is tier-classified as an `{kind:'agent'}` actor, since by
16
- // construction only an autonomous agent process can reach the bridge.
9
+ // The ACP agent process drives its OWN tool-calling loop and calls domain
10
+ // tools through the Rust-side MCP bridge so dispatch is triggered by a
11
+ // STANDING listener on `acp://mcp-tool-call`, not by anything in
12
+ // request()/respond(). Every such call is tier-classified as an
13
+ // `{kind:'agent'}` actor, since by construction only an autonomous agent
14
+ // process can reach the bridge.
17
15
  //
18
16
  // Scope: one active session/request at a time, matching how AgentDialog is
19
17
  // actually used today (a single live conversation). A tool call or permission
@@ -43,7 +41,7 @@ function finalizeTurn(turn) {
43
41
  if (turn.stopped === 'max_steps' || turn.stopped === 'refusal') status = 'partial'
44
42
  else if (turn.stopped === 'cancelled') status = 'partial'
45
43
  else if (question) status = 'needs_clarification'
46
- return { status, summary: question ? null : (turn.content || null), question }
44
+ return { status, summary: question ? null : turn.content || null, question }
47
45
  }
48
46
 
49
47
  /**
@@ -53,9 +51,15 @@ function finalizeTurn(turn) {
53
51
  * @param {(tool: object, input: object) => unknown} [config.transport] backend runner for domain tools; omit for a chat-only kit (no domain MCP tools)
54
52
  * @param {Record<string, number>} [config.actorTiers] max executable tier rank per actor kind
55
53
  * @param {object} [config.deps] injectable `acp-agent.js` functions (tests only — defaults to the real module)
56
- * @returns {{request: Function, respond: Function, approve: Function}} bound kit
54
+ * @returns {{request: (opts: {intent: string, agent: object}) => Promise<object>, respond: (opts: {requestId: string, message: string}) => Promise<object>, approve: (opts: {requestId: string, approve: boolean}) => Promise<object>}} bound kit
57
55
  */
58
- export function createAcpAgentKit({ catalog, journal, transport, actorTiers = DEFAULT_ACTOR_TIERS, deps = acpAgent } = {}) {
56
+ export function createAcpAgentKit({
57
+ catalog,
58
+ journal,
59
+ transport,
60
+ actorTiers = DEFAULT_ACTOR_TIERS,
61
+ deps = acpAgent
62
+ } = {}) {
59
63
  if (!Array.isArray(catalog)) throw new Error('createAcpAgentKit: catalog (array) is required')
60
64
 
61
65
  const dispatch = transport ? createDispatch(catalog, transport) : null
@@ -71,17 +75,26 @@ export function createAcpAgentKit({ catalog, journal, transport, actorTiers = DE
71
75
  */
72
76
  async function handleToolCall({ requestId, tool, input }) {
73
77
  if (!dispatch) {
74
- await deps.respondAcpToolCall(requestId, { ok: false, error: { code: 'forbidden', message: 'No transport configured.' } })
78
+ await deps.respondAcpToolCall(requestId, {
79
+ ok: false,
80
+ error: { code: 'forbidden', message: 'No transport configured.' }
81
+ })
75
82
  return
76
83
  }
77
84
  const decision = classify(catalog, actorTiers, AGENT_ACTOR, tool)
78
85
  if (decision === 'deny') {
79
- await deps.respondAcpToolCall(requestId, { ok: false, error: { code: 'forbidden', message: `Tool "${tool}" is not allowed.` } })
86
+ await deps.respondAcpToolCall(requestId, {
87
+ ok: false,
88
+ error: { code: 'forbidden', message: `Tool "${tool}" is not allowed.` }
89
+ })
80
90
  return
81
91
  }
82
92
  if (decision === 'approval') {
83
93
  if (activeRequestId) {
84
- await journal.update(activeRequestId, { status: 'needs_approval', pendingApproval: { kind: 'mcp', requestId, tool, input } })
94
+ await journal.update(activeRequestId, {
95
+ status: 'needs_approval',
96
+ pendingApproval: { kind: 'mcp', requestId, tool, input }
97
+ })
85
98
  }
86
99
  return // acp_mcp_tool_result comes later, from approve()
87
100
  }
@@ -127,8 +140,7 @@ export function createAcpAgentKit({ catalog, journal, transport, actorTiers = DE
127
140
  let turn
128
141
  try {
129
142
  turn = await turnPromise
130
- }
131
- catch (error) {
143
+ } catch (error) {
132
144
  await journal.update(requestId, { status: 'failed', error: String(error?.message ?? error) })
133
145
  return { requestId, status: 'failed', summary: null, actions: baseActions, question: null, pendingApproval: null }
134
146
  }
@@ -163,18 +175,35 @@ export function createAcpAgentKit({ catalog, journal, transport, actorTiers = DE
163
175
  */
164
176
  async respond({ requestId, message }) {
165
177
  if (!activeSessionKey) {
166
- return { requestId, status: 'failed', summary: null, actions: [], question: 'No active ACP session.', pendingApproval: null }
178
+ return {
179
+ requestId,
180
+ status: 'failed',
181
+ summary: null,
182
+ actions: [],
183
+ question: 'No active ACP session.',
184
+ pendingApproval: null
185
+ }
167
186
  }
168
187
  let record
169
188
  try {
170
189
  record = await journal.load(requestId)
171
- }
172
- catch {
173
- return { requestId, status: 'failed', summary: null, actions: [], question: 'Request not found.', pendingApproval: null }
190
+ } catch {
191
+ return {
192
+ requestId,
193
+ status: 'failed',
194
+ summary: null,
195
+ actions: [],
196
+ question: 'Request not found.',
197
+ pendingApproval: null
198
+ }
174
199
  }
175
200
  await journal.update(requestId, { status: 'running' })
176
201
  activeRequestId = requestId
177
- return runAndJournal(requestId, record.actions ?? [], deps.runAcpTurn({ sessionKey: activeSessionKey, text: message }))
202
+ return runAndJournal(
203
+ requestId,
204
+ record.actions ?? [],
205
+ deps.runAcpTurn({ sessionKey: activeSessionKey, text: message })
206
+ )
178
207
  },
179
208
 
180
209
  /**
@@ -187,19 +216,32 @@ export function createAcpAgentKit({ catalog, journal, transport, actorTiers = DE
187
216
  let record
188
217
  try {
189
218
  record = await journal.load(requestId)
190
- }
191
- catch {
192
- return { requestId, status: 'failed', summary: null, actions: [], question: 'Request not found.', pendingApproval: null }
219
+ } catch {
220
+ return {
221
+ requestId,
222
+ status: 'failed',
223
+ summary: null,
224
+ actions: [],
225
+ question: 'Request not found.',
226
+ pendingApproval: null
227
+ }
193
228
  }
194
229
 
195
230
  const pending = record.pendingApproval
196
231
  if (record.status !== 'needs_approval' || !pending) {
197
- return { requestId, status: record.status, summary: record.summary, actions: record.actions ?? [], question: null, pendingApproval: null }
232
+ return {
233
+ requestId,
234
+ status: record.status,
235
+ summary: record.summary,
236
+ actions: record.actions ?? [],
237
+ question: null,
238
+ pendingApproval: null
239
+ }
198
240
  }
199
241
 
200
242
  if (pending.kind === 'acp') return approveAcpPermission(deps, journal, requestId, record, pending, decision)
201
243
  return approveMcpToolCall(deps, dispatch, journal, requestId, record, pending, decision)
202
- },
244
+ }
203
245
  }
204
246
  }
205
247
 
@@ -216,7 +258,7 @@ export function createAcpAgentKit({ catalog, journal, transport, actorTiers = DE
216
258
  * means the old "retry a failed dispatch" UX doesn't carry over: once the
217
259
  * agent has been told a tool call failed, it has already moved on.
218
260
  * @param {object} deps injected acp-agent.js functions
219
- * @param {Function} dispatch bound `createDispatch` result
261
+ * @param {(name: string, input?: object) => Promise<object>} dispatch bound `createDispatch` result
220
262
  * @param {object} journal journal store
221
263
  * @param {string} requestId journal record id
222
264
  * @param {object} record loaded journal record
@@ -256,5 +298,12 @@ async function approveAcpPermission(deps, journal, requestId, record, pending, d
256
298
  await deps.respondAcpPermission(pending.requestId, option?.optionId)
257
299
 
258
300
  await journal.update(requestId, { status: 'running', pendingApproval: null })
259
- return { requestId, status: 'running', summary: null, actions: record.actions ?? [], question: null, pendingApproval: null }
301
+ return {
302
+ requestId,
303
+ status: 'running',
304
+ summary: null,
305
+ actions: record.actions ?? [],
306
+ question: null,
307
+ pendingApproval: null
308
+ }
260
309
  }