@miadi/connector 0.1.9 → 0.1.11
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.
- package/README.md +11 -2
- package/dist/approve.d.ts +5 -0
- package/dist/approve.js +30 -1
- package/dist/episode-files.d.ts +62 -0
- package/dist/episode-files.js +115 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/mission-status.d.ts +87 -0
- package/dist/mission-status.js +144 -0
- package/dist/providers.d.ts +61 -0
- package/dist/providers.js +120 -0
- package/dist/records.d.ts +1 -1
- package/dist/records.js +1 -0
- package/dist/request-state.d.ts +1 -1
- package/dist/request-state.js +8 -1
- package/dist/server.d.ts +8 -0
- package/dist/server.js +16 -1
- package/dist/steer.d.ts +71 -0
- package/dist/steer.js +229 -0
- package/dist/tools.d.ts +9 -0
- package/dist/tools.js +303 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/tools.js
CHANGED
|
@@ -18,9 +18,13 @@ import { CIRCLES, CIRCLE_IDS, CLAIM_CATEGORIES, DOMAINS, HANDOFF_STATES, current
|
|
|
18
18
|
import { VERSION } from "./version.js";
|
|
19
19
|
import { AlreadyDecided, DECISIONS, KIND_LABELS, consequenceOf, decide, isApprovable, readQueueState, requestProjection } from "./approve.js";
|
|
20
20
|
import { DECISIONS_CARD_HTML, DECISIONS_CARD_URI, DECISION_TOKENS_META } from "./decisions.js";
|
|
21
|
+
import { discoverProviders, resolveProvider, routerPreflight, PROVIDERS } from "./providers.js";
|
|
22
|
+
import { missionStatus } from "./mission-status.js";
|
|
23
|
+
import { checkGrant, isBusy, steerAttempts, steeringGrantOf, steerSession } from "./steer.js";
|
|
21
24
|
import { join } from "node:path";
|
|
22
25
|
import { artefactVisibility, readArtefactWeave, saveResearchArtefact } from "@miadi/inquiry-weave";
|
|
23
26
|
import { ResearchUploads, UploadRefusal } from "./uploads.js";
|
|
27
|
+
import { DEFAULT_PAGE_BYTES, FileRefusal, MAX_TEXT_BYTES, MIN_PAGE_BYTES, TEXT_TYPES, checkRelativePath, episodeNumberOf, extensionOf, fileRef, pageOf, parseFileRef, } from "./episode-files.js";
|
|
24
28
|
class ToolRefusal extends Error {
|
|
25
29
|
code;
|
|
26
30
|
detail;
|
|
@@ -60,6 +64,8 @@ function wrap(fn) {
|
|
|
60
64
|
return fail({ error: error.code, message: error.message, ...(error.detail ? { detail: error.detail } : {}) });
|
|
61
65
|
if (error instanceof UploadRefusal)
|
|
62
66
|
return fail({ error: "invalid_upload", message: error.message });
|
|
67
|
+
if (error instanceof FileRefusal)
|
|
68
|
+
return fail({ error: error.code, message: error.message });
|
|
63
69
|
if (error instanceof MiadiError) {
|
|
64
70
|
const unavailable = error.status === 0;
|
|
65
71
|
return fail({
|
|
@@ -324,12 +330,105 @@ export function registerTools(server, ctx) {
|
|
|
324
330
|
]);
|
|
325
331
|
return {
|
|
326
332
|
episode: decodeURIComponent(ref),
|
|
327
|
-
files,
|
|
333
|
+
files: withReadRefs(files),
|
|
328
334
|
screenwalks,
|
|
329
335
|
reviews: epReviews,
|
|
330
336
|
connector_records: touching(`miadi-chronicle://${decodeURIComponent(ref)}`),
|
|
331
337
|
};
|
|
332
338
|
}));
|
|
339
|
+
/**
|
|
340
|
+
* One text file of one episode, read as the person through Miadi's file
|
|
341
|
+
* door; Miadi applies their audience, the room rule and the vessel's path
|
|
342
|
+
* guard, and the path is checked here before a URL is built (jgwill/Miadi#815).
|
|
343
|
+
*/
|
|
344
|
+
const readEpisodeFile = async (person, a) => {
|
|
345
|
+
const path = a.path.trim();
|
|
346
|
+
const segments = checkRelativePath(path);
|
|
347
|
+
const extension = extensionOf(path);
|
|
348
|
+
const contentType = TEXT_TYPES[extension];
|
|
349
|
+
if (!contentType) {
|
|
350
|
+
throw new FileRefusal("unsupported", `${path} is not read as text here: only ${Object.keys(TEXT_TYPES).join(", ")} files are. Miadi serves documents (html, pdf, csv, svg) as downloads and images and media by their own routes; get_evidence lists them.`);
|
|
351
|
+
}
|
|
352
|
+
const ref = episodeRef(a.episode);
|
|
353
|
+
// A number, epNNN or a folder name; `..` would be folded by the URL parser into another route.
|
|
354
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(decodeURIComponent(ref))) {
|
|
355
|
+
throw new FileRefusal("invalid_path", "Name the episode as miadi-chronicle://<n>, its number or its folder.");
|
|
356
|
+
}
|
|
357
|
+
const url = `/api/chronicle/episodes/${ref}/files/${segments.map(encodeURIComponent).join("/")}`;
|
|
358
|
+
let text;
|
|
359
|
+
try {
|
|
360
|
+
text = await miadi.get(person.token, url);
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
if (!(error instanceof MiadiError))
|
|
364
|
+
throw error;
|
|
365
|
+
const body = (error.body ?? {});
|
|
366
|
+
const detail = { episode: decodeURIComponent(ref), path, miadi: { status: error.status, ...body } };
|
|
367
|
+
if (error.status === 0)
|
|
368
|
+
throw new ToolRefusal("unavailable", "Miadi could not be reached.", detail);
|
|
369
|
+
if (body.error === "not_in_the_room")
|
|
370
|
+
throw new ToolRefusal("forbidden", body.message ?? "This file is not part of the episode room.", detail);
|
|
371
|
+
if (error.status === 401 || error.status === 403)
|
|
372
|
+
throw new ToolRefusal("forbidden", body.message ?? "Miadi does not let this person read that file.", detail);
|
|
373
|
+
if (error.status === 404)
|
|
374
|
+
throw new ToolRefusal("not_found", body.message ?? `No file ${path} in ${decodeURIComponent(ref)}.`, detail);
|
|
375
|
+
if (body.error === "TOO_LARGE")
|
|
376
|
+
throw new ToolRefusal("too_large", `${path} is larger than the ${MAX_TEXT_BYTES / 1024} KiB Miadi reads as text whole; nothing of it is returned.`, detail);
|
|
377
|
+
if (error.status === 415)
|
|
378
|
+
throw new ToolRefusal("unsupported", body.message ?? `${path} is not text Miadi reads.`, detail);
|
|
379
|
+
if (error.status === 400)
|
|
380
|
+
throw new ToolRefusal("invalid_path", body.message ?? `${path} is not a readable path in the episode.`, detail);
|
|
381
|
+
throw error;
|
|
382
|
+
}
|
|
383
|
+
if (typeof text?.content !== "string")
|
|
384
|
+
throw new ToolRefusal("unsupported", `Miadi did not return ${path} as text.`);
|
|
385
|
+
const number = episodeNumberOf(text.episode);
|
|
386
|
+
const episodeAddress = number === null ? `chronicle:${text.episode}` : `miadi-chronicle://${number}`;
|
|
387
|
+
return {
|
|
388
|
+
ref: fileRef(number ?? text.episode, text.file ?? path),
|
|
389
|
+
episode: { ref: episodeAddress, number, folder: text.episode },
|
|
390
|
+
path: text.file ?? path,
|
|
391
|
+
content_type: contentType,
|
|
392
|
+
...pageOf(text, { offset: a.offset, maxBytes: a.max_bytes, revision: a.revision }),
|
|
393
|
+
};
|
|
394
|
+
};
|
|
395
|
+
/** Each text file Miadi lists as previewable carries the ref read_episode_file and fetch take. */
|
|
396
|
+
const withReadRefs = (listing) => {
|
|
397
|
+
const folder = listing?.episode?.path;
|
|
398
|
+
if (!folder || !Array.isArray(listing?.files))
|
|
399
|
+
return listing;
|
|
400
|
+
const number = listing.episode.number ?? episodeNumberOf(folder);
|
|
401
|
+
return {
|
|
402
|
+
...listing,
|
|
403
|
+
files: listing.files.map((f) => f?.kind === "text" && f.previewable && TEXT_TYPES[extensionOf(String(f.relativePath))]
|
|
404
|
+
? { ...f, read_ref: fileRef(number ?? folder, f.relativePath) }
|
|
405
|
+
: f),
|
|
406
|
+
connector_read: "A file with read_ref is read whole, in pages if long, by read_episode_file or fetch. Files without one are listed only.",
|
|
407
|
+
};
|
|
408
|
+
};
|
|
409
|
+
server.registerTool("read_episode_file", {
|
|
410
|
+
title: "Read one file an episode holds",
|
|
411
|
+
description: "The full UTF-8 text of one text file (md, txt, yaml, yml, json) in a Chronicle episode, read as you through Miadi, with its content type, byte count and sha256. Name the episode and the path get_evidence lists (or pass its read_ref as episode alone). A file longer than max_bytes comes in pages: pass page.next back to read the next one; joined, the pages hash to sha256. Read only: nothing is changed.",
|
|
412
|
+
inputSchema: {
|
|
413
|
+
episode: z.string().min(1).describe("miadi-chronicle://<n>, an episode number or its folder; or a read_ref (miadi-chronicle://<n>/<path>) with path left out."),
|
|
414
|
+
path: z.string().optional().describe("The file's path inside the episode, as get_evidence lists it, e.g. foundations/broker-adapters/README.md."),
|
|
415
|
+
offset: z.number().int().min(0).optional().describe("0, or page.next.offset from the previous page."),
|
|
416
|
+
revision: z.string().regex(/^[0-9a-f]{64}$/).optional().describe("page.next.revision from the previous page: refuses with changed if the file moved since."),
|
|
417
|
+
max_bytes: z.number().int().min(MIN_PAGE_BYTES).max(MAX_TEXT_BYTES).optional().describe(`Page size in bytes (default ${DEFAULT_PAGE_BYTES}).`),
|
|
418
|
+
},
|
|
419
|
+
annotations: READ,
|
|
420
|
+
}, wrap(async (a, extra) => {
|
|
421
|
+
const { person } = who(extra);
|
|
422
|
+
let episode = a.episode;
|
|
423
|
+
let path = a.path;
|
|
424
|
+
if (!path) {
|
|
425
|
+
const parsed = parseFileRef(a.episode);
|
|
426
|
+
if (!parsed)
|
|
427
|
+
throw new FileRefusal("invalid_path", "Name the file: give path, or pass a read_ref (miadi-chronicle://<n>/<path>) as episode.");
|
|
428
|
+
({ episode, path } = parsed);
|
|
429
|
+
}
|
|
430
|
+
return readEpisodeFile(person, { ...a, episode, path });
|
|
431
|
+
}));
|
|
333
432
|
server.registerTool("get_evaluations", {
|
|
334
433
|
title: "Evaluations: provider traces and recorded assessments",
|
|
335
434
|
description: "The tracing records (LLMOps provider traces and prompts) an episode holds, and the assessments people recorded with record_evaluation, operational and creator-experience kept apart.",
|
|
@@ -468,6 +567,72 @@ export function registerTools(server, ctx) {
|
|
|
468
567
|
: {}),
|
|
469
568
|
};
|
|
470
569
|
}));
|
|
570
|
+
server.registerTool("mission_status", {
|
|
571
|
+
title: "Where a mission stands",
|
|
572
|
+
description: "One mission, joined and verified: its source request and decision, the sessions bound to it (live or not), its issues and PRs, what is approved, queued, delivered, running, merged, released or closed — each by its evidence, never inferred from an idle pane — and the next authorized action. Operator details need Miadi's write tier. jgwill/Miadi#893.",
|
|
573
|
+
inputSchema: {
|
|
574
|
+
ref: z
|
|
575
|
+
.string()
|
|
576
|
+
.min(1)
|
|
577
|
+
.describe("run_request:<id>, work_proposal:<id>, another record id, session:<uuid>, or owner/repo#n the mission cites."),
|
|
578
|
+
},
|
|
579
|
+
annotations: READ,
|
|
580
|
+
}, wrap(async ({ ref }, extra) => {
|
|
581
|
+
const { person } = who(extra);
|
|
582
|
+
const writeTier = person.api_tier === "write";
|
|
583
|
+
return missionStatus(ref.trim(), records(), {
|
|
584
|
+
projectOf: (r) => requestProjection(r, { reviewBase: ctx.approval?.config.reviewUrl, factoryDir: ctx.factoryDir }),
|
|
585
|
+
queueOf: (r) => (r.kind === "run_request" || r.kind === "work_proposal" ? readQueueState(r, ctx.factoryDir) : undefined),
|
|
586
|
+
tmuxSessions: writeTier ? (ctx.tmuxSessions ?? (() => [])) : undefined,
|
|
587
|
+
paneBusy: writeTier && ctx.tmux
|
|
588
|
+
? (session) => {
|
|
589
|
+
try {
|
|
590
|
+
const pane = ctx.tmux.listPanes(session)[0];
|
|
591
|
+
if (!pane)
|
|
592
|
+
return "unknown";
|
|
593
|
+
return isBusy(ctx.tmux.capture(`${session}:${pane.window}.${pane.pane}`)) ? "busy" : "idle";
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
return "unknown";
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
: undefined,
|
|
600
|
+
sessionOf: writeTier ? (id) => miadi.get(person.token, `/api/session/${encodeURIComponent(id)}`) : undefined,
|
|
601
|
+
issueOf: async (owner, repo, n) => {
|
|
602
|
+
const f = ctx.approval?.fetchImpl ?? fetch;
|
|
603
|
+
const headers = {
|
|
604
|
+
accept: "application/vnd.github+json",
|
|
605
|
+
...(process.env.GITHUB_TOKEN ? { authorization: "Bearer " + process.env.GITHUB_TOKEN } : {}),
|
|
606
|
+
};
|
|
607
|
+
const pr = await f(`https://api.github.com/repos/${owner}/${repo}/pulls/${n}`, { headers, signal: AbortSignal.timeout(15_000) });
|
|
608
|
+
if (pr.ok) {
|
|
609
|
+
const p = await pr.json();
|
|
610
|
+
return { state: p.state, title: p.title, merged: Boolean(p.merged_at) };
|
|
611
|
+
}
|
|
612
|
+
const res = await f(`https://api.github.com/repos/${owner}/${repo}/issues/${n}`, { headers, signal: AbortSignal.timeout(15_000) });
|
|
613
|
+
if (!res.ok)
|
|
614
|
+
return { state: res.status === 404 ? "not_found_or_private" : `github_${res.status}` };
|
|
615
|
+
const i = await res.json();
|
|
616
|
+
return { state: i.state, title: i.title };
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
}));
|
|
620
|
+
server.registerTool("provider_capabilities", {
|
|
621
|
+
title: "Which agent providers this host can launch",
|
|
622
|
+
description: "The provider reality of the connector host (jgwill/Miadi#893): which agent CLIs are installed, their versions, whether sign-in evidence exists, the preference order (Codex, Pi, Copilot; Claude Code only on explicit request), and the read-only Jev routing preflight. No token is read or shown. Needs Miadi's write tier: this is host truth.",
|
|
623
|
+
inputSchema: {},
|
|
624
|
+
annotations: READ,
|
|
625
|
+
}, wrap(async (_args, extra) => {
|
|
626
|
+
const { person } = who(extra);
|
|
627
|
+
requireTier(person, "write");
|
|
628
|
+
return {
|
|
629
|
+
preference_order: PROVIDERS.map((p) => p.id),
|
|
630
|
+
explicit_only: PROVIDERS.filter((p) => !p.selectable_by_default).map((p) => p.id),
|
|
631
|
+
providers: discoverProviders(ctx.providersProbe),
|
|
632
|
+
router: await routerPreflight(process.env, ctx.approval?.fetchImpl ?? fetch),
|
|
633
|
+
note: "A provider name in a task's text selects nothing; request_authorized_run's provider field is verified against this list, and an unavailable provider leaves the mission unstarted.",
|
|
634
|
+
};
|
|
635
|
+
}));
|
|
471
636
|
// search / fetch: the pair ChatGPT uses for connectors in search and deep research.
|
|
472
637
|
server.registerTool("search", {
|
|
473
638
|
title: "Search Miadi",
|
|
@@ -494,11 +659,29 @@ export function registerTools(server, ctx) {
|
|
|
494
659
|
}));
|
|
495
660
|
server.registerTool("fetch", {
|
|
496
661
|
title: "Fetch one item found by search",
|
|
497
|
-
description: "The full text of an item returned by search: a review (miadi-review:<uuid>) or a connector record id.",
|
|
662
|
+
description: "The full text of an item returned by search: a review (miadi-review:<uuid>) or a connector record id; or of an episode file by the read_ref get_evidence lists (miadi-chronicle://<n>/<path>), its first page when long.",
|
|
498
663
|
inputSchema: { id: z.string().min(1) },
|
|
499
664
|
annotations: READ,
|
|
500
665
|
}, wrap(async ({ id }, extra) => {
|
|
501
|
-
who(extra);
|
|
666
|
+
const { person } = who(extra);
|
|
667
|
+
const file = parseFileRef(id);
|
|
668
|
+
if (file) {
|
|
669
|
+
const read = await readEpisodeFile(person, file);
|
|
670
|
+
return {
|
|
671
|
+
id: read.ref,
|
|
672
|
+
title: `${read.path} (${read.episode.ref})`,
|
|
673
|
+
text: read.text,
|
|
674
|
+
url: `${config.miadiPublicUrl}/chronicle/${encodeURIComponent(read.episode.folder)}`,
|
|
675
|
+
metadata: {
|
|
676
|
+
content_type: read.content_type,
|
|
677
|
+
bytes: read.bytes,
|
|
678
|
+
sha256: read.sha256,
|
|
679
|
+
verified: read.verified,
|
|
680
|
+
complete: read.page.complete,
|
|
681
|
+
...(read.page.next ? { next: { tool: "read_episode_file", episode: read.ref, ...read.page.next } } : {}),
|
|
682
|
+
},
|
|
683
|
+
};
|
|
684
|
+
}
|
|
502
685
|
const reviewId = reviewIdOf(id);
|
|
503
686
|
if (reviewId) {
|
|
504
687
|
const r = await reviews.get(reviewId);
|
|
@@ -815,12 +998,31 @@ export function registerTools(server, ctx) {
|
|
|
815
998
|
refs,
|
|
816
999
|
session: z.string().regex(/^[A-Za-z0-9._-]{3,80}$/).optional().describe("An existing tmux session to hand it to, or a name for a new one."),
|
|
817
1000
|
cwd: z.string().max(300).optional().describe("Working folder for a new session (default the Miadi checkout)."),
|
|
1001
|
+
provider: z
|
|
1002
|
+
.enum(["codex", "pi", "copilot", "claude"])
|
|
1003
|
+
.optional()
|
|
1004
|
+
.describe("The provider the run should launch with, verified installed on the connector host. Claude Code runs only when named explicitly. Unknown or unavailable leaves the mission unstarted."),
|
|
1005
|
+
steering: z
|
|
1006
|
+
.object({
|
|
1007
|
+
max_attempts: z.number().int().min(1).max(20).describe("How many authorized follow-up steers this approval covers."),
|
|
1008
|
+
expires_at: z.string().optional().describe("ISO time after which no steer runs under this grant."),
|
|
1009
|
+
pane: z.string().regex(/^\d+\.\d+$/).optional().describe("The pane the seat lives in (window.pane); default the session's first pane."),
|
|
1010
|
+
})
|
|
1011
|
+
.optional()
|
|
1012
|
+
.describe("The steering grant the approver covers with this run: bounded follow-ups to the same session through request_session_steer."),
|
|
818
1013
|
...base,
|
|
819
1014
|
},
|
|
820
1015
|
annotations: WRITE,
|
|
821
1016
|
}, wrap(async (a, extra) => {
|
|
822
1017
|
const { person, auth } = who(extra);
|
|
823
1018
|
requireContribute(auth);
|
|
1019
|
+
const found = discoverProviders(ctx.providersProbe);
|
|
1020
|
+
const resolution = resolveProvider(a.provider, found);
|
|
1021
|
+
if (resolution.state === "provider_unknown")
|
|
1022
|
+
throw new ToolRefusal("invalid_request", resolution.message, { available: resolution.available });
|
|
1023
|
+
if (resolution.state !== "ok") {
|
|
1024
|
+
return { state: resolution.state, message: resolution.message, available: resolution.available, note: "Nothing was recorded or queued; the mission is unstarted." };
|
|
1025
|
+
}
|
|
824
1026
|
const r = append("run_request", person, auth, {
|
|
825
1027
|
circle: a.circle,
|
|
826
1028
|
refs: a.refs,
|
|
@@ -829,9 +1031,100 @@ export function registerTools(server, ctx) {
|
|
|
829
1031
|
idempotency_key: a.idempotency_key,
|
|
830
1032
|
approval_state: "awaiting_approval",
|
|
831
1033
|
state: "pending",
|
|
832
|
-
body: {
|
|
1034
|
+
body: {
|
|
1035
|
+
task: a.task,
|
|
1036
|
+
session: a.session,
|
|
1037
|
+
cwd: a.cwd,
|
|
1038
|
+
provider: a.provider,
|
|
1039
|
+
resolved_provider: resolution.provider.id,
|
|
1040
|
+
...(a.steering ? { steering: a.steering } : {}),
|
|
1041
|
+
},
|
|
833
1042
|
});
|
|
834
|
-
return {
|
|
1043
|
+
return {
|
|
1044
|
+
...r,
|
|
1045
|
+
provider: { requested: a.provider ?? null, resolved: resolution.provider.id, explicit: resolution.explicit, version: resolution.provider.version },
|
|
1046
|
+
approval_url: approvalUrl(r.id),
|
|
1047
|
+
};
|
|
1048
|
+
}));
|
|
1049
|
+
server.registerTool("request_session_steer", {
|
|
1050
|
+
title: "Send one authorized follow-up to a running agent",
|
|
1051
|
+
description: "Steer one existing live agent session within an approved run's steering grant (jgwill/Miadi#893): the exact instruction is typed into the mission's bound pane — never whichever pane is active — after a peek, and a re-peek verifies the agent accepted it. When the approved run carries no steering grant covering this, the steer waits for one explicit scoped human decision on the returned approval_url. Every attempt leaves a durable artifact with its hash and both captures.",
|
|
1052
|
+
inputSchema: {
|
|
1053
|
+
run_request: z.string().min(1).describe("The approved run_request or work_proposal whose steering grant covers this steer."),
|
|
1054
|
+
instruction: z.string().min(1).max(4000).describe("The exact instruction typed into the session. It is material within the approved mission's scope, not new authority."),
|
|
1055
|
+
session: z.string().regex(/^[A-Za-z0-9._-]{3,80}$/).optional().describe("The session to steer; must be the one the grant binds."),
|
|
1056
|
+
pane: z.string().regex(/^\d+\.\d+$/).optional().describe("The pane (window.pane); default the grant's pane, else the session's first pane. The active pane is never used."),
|
|
1057
|
+
refs,
|
|
1058
|
+
...base,
|
|
1059
|
+
},
|
|
1060
|
+
annotations: WRITE,
|
|
1061
|
+
}, wrap(async (a, extra) => {
|
|
1062
|
+
const { person, auth } = who(extra);
|
|
1063
|
+
requireContribute(auth);
|
|
1064
|
+
requireTier(person, "write");
|
|
1065
|
+
if (!ctx.tmux)
|
|
1066
|
+
throw new ToolRefusal("unavailable", "The connector host has no steering surface; the steer is unstarted.");
|
|
1067
|
+
const grantRecord = all().find((r) => r.id === a.run_request);
|
|
1068
|
+
if (!grantRecord || (grantRecord.kind !== "run_request" && grantRecord.kind !== "work_proposal")) {
|
|
1069
|
+
throw new ToolRefusal("invalid_ref", "Give the id of the approved run_request or work_proposal this steer belongs to.");
|
|
1070
|
+
}
|
|
1071
|
+
if (grantRecord.approval_state !== "approved") {
|
|
1072
|
+
throw new ToolRefusal("not_approved", `${grantRecord.id} is ${grantRecord.approval_state ?? "not approved"}; a steer needs an approved run's grant or its own human decision.`);
|
|
1073
|
+
}
|
|
1074
|
+
const grant = steeringGrantOf(grantRecord);
|
|
1075
|
+
const priorAttempts = steerAttempts(all(), grantRecord.id);
|
|
1076
|
+
const covered = checkGrant(grant, priorAttempts) === "ok";
|
|
1077
|
+
const body = {
|
|
1078
|
+
instruction: a.instruction,
|
|
1079
|
+
session: a.session ?? grantRecord.body.session,
|
|
1080
|
+
pane: a.pane ?? grant?.pane,
|
|
1081
|
+
};
|
|
1082
|
+
const fields = {
|
|
1083
|
+
circle: grantRecord.circle,
|
|
1084
|
+
refs: [grantRecord.id, ...a.refs],
|
|
1085
|
+
intention: a.intention,
|
|
1086
|
+
role: a.role,
|
|
1087
|
+
idempotency_key: a.idempotency_key,
|
|
1088
|
+
body,
|
|
1089
|
+
};
|
|
1090
|
+
if (!covered) {
|
|
1091
|
+
// One explicit scoped human decision (jgwill/Miadi#893): the record waits
|
|
1092
|
+
// on the approval page; approving it runs exactly this instruction once.
|
|
1093
|
+
const r = append("session_steer_request", person, auth, { ...fields, approval_state: "awaiting_approval", state: "pending" });
|
|
1094
|
+
return {
|
|
1095
|
+
...r,
|
|
1096
|
+
approval_url: approvalUrl(r.id),
|
|
1097
|
+
state: "awaiting_approval",
|
|
1098
|
+
reason: !grant ? "no_grant" : checkGrant(grant, priorAttempts),
|
|
1099
|
+
message: "The approved run's steering grant does not cover this steer; a person's decision is requested, once, for exactly this instruction.",
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
const r = append("session_steer_request", person, auth, { ...fields, approval_state: "not_required" });
|
|
1103
|
+
const result = await steerSession({ tmux: ctx.tmux, stateDir: config.stateDir, ...(ctx.steerSettleMs !== undefined ? { settleMs: ctx.steerSettleMs } : {}) }, { record: r, grantRecord, priorAttempts });
|
|
1104
|
+
const delivered = result.status === "accepted" || result.status === "submitted";
|
|
1105
|
+
ledger.append({
|
|
1106
|
+
id: newRecordId(r.kind),
|
|
1107
|
+
kind: r.kind,
|
|
1108
|
+
created_at: new Date().toISOString(),
|
|
1109
|
+
actor: r.actor,
|
|
1110
|
+
refs: r.refs,
|
|
1111
|
+
evidence_refs: [],
|
|
1112
|
+
supersedes: r.id,
|
|
1113
|
+
approval_state: "not_required",
|
|
1114
|
+
state: delivered ? "completed" : "blocked",
|
|
1115
|
+
completion_receipt: {
|
|
1116
|
+
decision: "covered_by_grant",
|
|
1117
|
+
decided_by: { person_id: person.id, name: person.name },
|
|
1118
|
+
decided_at: new Date().toISOString(),
|
|
1119
|
+
outcome: delivered ? result.status : "failed",
|
|
1120
|
+
steer_status: result.status,
|
|
1121
|
+
message: result.message,
|
|
1122
|
+
...(result.target ? { target: result.target } : {}),
|
|
1123
|
+
...(result.artifact ? { artifact: result.artifact } : {}),
|
|
1124
|
+
},
|
|
1125
|
+
body: r.body,
|
|
1126
|
+
});
|
|
1127
|
+
return records().find((x) => x.id === r.id) ?? r;
|
|
835
1128
|
}));
|
|
836
1129
|
server.registerTool("request_review_operation", {
|
|
837
1130
|
title: "Request a review operation",
|
|
@@ -1255,6 +1548,7 @@ export const TOOL_STATUS = {
|
|
|
1255
1548
|
list_sessions: "live, Miadi write tier",
|
|
1256
1549
|
get_session: "live, Miadi write tier",
|
|
1257
1550
|
get_evidence: "live",
|
|
1551
|
+
read_episode_file: "live (text files an episode holds: md, txt, yaml, yml, json, up to 512 KiB, paged; read as you, nothing changed)",
|
|
1258
1552
|
get_evaluations: "live (tracing records and recorded assessments; no provider scores)",
|
|
1259
1553
|
get_narrative_beats: "live (NCP project beats and candidate beats)",
|
|
1260
1554
|
review_lookup: "live (video search needs the review token on the connector host)",
|
|
@@ -1262,6 +1556,8 @@ export const TOOL_STATUS = {
|
|
|
1262
1556
|
handoff_status: "live",
|
|
1263
1557
|
list_records: "live",
|
|
1264
1558
|
get_run_result: "live",
|
|
1559
|
+
mission_status: "live (operator details need the Miadi write tier)",
|
|
1560
|
+
provider_capabilities: "live, Miadi write tier",
|
|
1265
1561
|
search: "live",
|
|
1266
1562
|
fetch: "live",
|
|
1267
1563
|
append_observation: "live, contribute",
|
|
@@ -1275,7 +1571,8 @@ export const TOOL_STATUS = {
|
|
|
1275
1571
|
propose_work: "live, contribute, needs_approval (approving starts the work as a factory run)",
|
|
1276
1572
|
prepare_calendar_event: "live, contribute (draft and .ics only)",
|
|
1277
1573
|
propose_session_completion: "live, contribute, needs_approval (decision recorded, nothing closed)",
|
|
1278
|
-
request_authorized_run: "live, contribute, needs_approval (queues a factory brief once approved)",
|
|
1574
|
+
request_authorized_run: "live, contribute, needs_approval (queues a factory brief once approved; may name a verified provider and a bounded steering grant)",
|
|
1575
|
+
request_session_steer: "live, contribute, write tier (runs inside an approved run's steering grant, else one scoped human decision)",
|
|
1279
1576
|
request_review_operation: "live, contribute, needs_approval",
|
|
1280
1577
|
request_deep_diver: "live, contribute, needs_approval",
|
|
1281
1578
|
link_review_to_episode: "live, contribute, needs_approval",
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.1.
|
|
1
|
+
export declare const VERSION = "0.1.11";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.1.
|
|
1
|
+
export const VERSION = "0.1.11";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miadi/connector",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "The Miadi connector: one remote MCP service through which a person signed in with their Miadi person token, from ChatGPT or any MCP client, reads sessions, reviews, episodes and circles and records observations, inquiries, handoffs and proposals with their name on each record.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Miadi",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"README.md"
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@miadi/inquiry-weave": "0.18.
|
|
29
|
+
"@miadi/inquiry-weave": "0.18.8",
|
|
30
30
|
"@miadi/node-service-kit": "0.1.2",
|
|
31
31
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
32
32
|
"express": "^5.2.1",
|