maqam 0.1.2 → 0.1.4

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 CHANGED
@@ -2,20 +2,24 @@
2
2
 
3
3
  ![Maqam governed agent framework hero](app/assets/maqam-readme-hero.png)
4
4
 
5
- Maqam is an MIT-licensed Ajnas agent framework for governed workflows. It combines a local agent runtime, policy engine, evidence ledger, skill registry, tool gateway, human-review-ready approval errors, and a crawler-backed research workflow.
5
+ Maqam is an MIT-licensed Ajnas agent framework for governed workflows. It combines a local agent runtime, policy engine, evidence ledger, skill registry, tool gateway, generic agent adapter, human-review-ready approval errors, and a crawler-backed research workflow.
6
6
 
7
- The crawler is no longer the product center; it is the first governed connector. Maqam is meant for enterprise agent workflows that need inspectable runs, source-backed outputs, compliance-friendly defaults, and no required hosted service.
7
+ The crawler is not the product center; it is only the first built-in connector. Maqam can govern any agent or tool you register through `ToolGateway`, including function agents, object agents with `run`/`invoke`/`call`, browser agents, research agents, internal SaaS connectors, and write-action agents that need human approval.
8
8
 
9
9
  Full documentation: [docs/usage.md](https://github.com/AjnasNB/maqam/blob/main/docs/usage.md)
10
10
 
11
11
  ![Maqam system map](app/assets/maqam-system-map.svg)
12
12
 
13
+ ![Maqam governed CLI worker flow](app/assets/maqam-cli-agent-flow.png)
14
+
13
15
  ## What Ships
14
16
 
15
17
  - `AgentRuntime`: sequential workflow execution with retries, trace events, task outputs, and policy preflight.
16
18
  - `PolicyEngine`: deterministic goal and tool-call decisions for allowed tools, origins, limits, and approval gates.
17
19
  - `EvidenceLedger`: provenance records, claim links, source hashes, confidence, and unsupported-claim checks.
18
20
  - `ToolGateway`: one governed path for external tool execution.
21
+ - `createAgentTool`: wraps any function agent or object agent so Maqam can control it through policy, trace, approval, and evidence.
22
+ - `createCliAgentTool`: wraps fixed command-line workers with timeout, approximate input-token limits, output byte limits, and no shell execution by default.
19
23
  - `SkillRegistry`: lightweight skill metadata registration and selection.
20
24
  - `createResearchWorkflow`: crawler-backed source collection, synthesis, and quality checks.
21
25
  - `maqam`: local web console for running governed research workflows.
@@ -80,18 +84,32 @@ import {
80
84
  EvidenceLedger,
81
85
  PolicyEngine,
82
86
  ToolGateway,
87
+ createAgentTool,
88
+ createCliAgentTool,
83
89
  createCrawlerTool,
84
90
  createResearchWorkflow
85
91
  } from "maqam";
86
92
 
87
93
  const evidenceLedger = new EvidenceLedger();
88
94
  const policyEngine = new PolicyEngine({
89
- allowedTools: ["crawler"],
95
+ allowedTools: ["crawler", "summarizer"],
90
96
  allowedOrigins: ["https://github.com", "https://www.npmjs.com"]
91
97
  });
92
98
 
93
99
  const gateway = new ToolGateway({ policyEngine, evidenceLedger });
94
100
  gateway.registerTool("crawler", createCrawlerTool());
101
+ gateway.registerTool("summarizer", createAgentTool(async (input) => ({
102
+ summary: `Reviewed ${input.topic}`
103
+ }), { name: "summarizer" }));
104
+ gateway.registerTool("localWorker", createCliAgentTool({
105
+ name: "localWorker",
106
+ command: process.execPath,
107
+ args: ["--version"],
108
+ stdin: "none",
109
+ timeoutMs: 5000,
110
+ maxInputTokens: 20,
111
+ maxOutputBytes: 2048
112
+ }));
95
113
 
96
114
  const runtime = new AgentRuntime({ policyEngine, evidenceLedger, toolGateway: gateway });
97
115
  const result = await runtime.runWorkflow(
@@ -101,7 +119,7 @@ const result = await runtime.runWorkflow(
101
119
  }),
102
120
  {
103
121
  objective: "Research permissive OSS agent framework projects",
104
- allowedTools: ["crawler"],
122
+ allowedTools: ["crawler", "summarizer"],
105
123
  allowedOrigins: ["https://github.com"]
106
124
  }
107
125
  );
@@ -149,7 +167,7 @@ Brand assets live in `app/assets/`, including `maqam-logo.svg` and `maqam-brand-
149
167
  - Use a clear user agent.
150
168
  - Rate-limit per origin.
151
169
  - Avoid bypassing access controls, paywalls, anti-bot systems, or private content.
152
- - No required AI provider dependency.
170
+ - No required model provider dependency.
153
171
  - No required external hosted service.
154
172
  - Produce JSON/JSONL output that agents can consume directly.
155
173
 
@@ -176,3 +194,7 @@ Publishing requires an authenticated npm session with permission to publish the
176
194
  ## License
177
195
 
178
196
  MIT
197
+
198
+ ## Open Development
199
+
200
+ Maqam is open source under MIT and open for development, issues, ideas, and contributions.
package/docs/usage.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Maqam Usage Guide
2
2
 
3
- Maqam is an MIT-licensed Ajnas agent framework for governed workflows. It gives you a small local runtime for building agent systems that can be inspected, policy-checked, and connected to evidence.
3
+ Maqam is an MIT-licensed Ajnas agent framework for governed workflows. It gives you a small local runtime for building agent systems that can be inspected, policy-checked, and connected to evidence. The crawler is only one built-in connector; Maqam can also govern arbitrary agents and tools through `createAgentTool` and `ToolGateway`.
4
4
 
5
5
  This guide covers installation, CLI usage, SDK usage, the local console, crawler usage, API reference, common patterns, and troubleshooting.
6
6
 
@@ -14,6 +14,8 @@ This guide covers installation, CLI usage, SDK usage, the local console, crawler
14
14
  - [Architecture](#architecture)
15
15
  - [API Reference](#api-reference)
16
16
  - [Build A Custom Workflow](#build-a-custom-workflow)
17
+ - [Control Any Agent](#control-any-agent)
18
+ - [Control CLI Workers](#control-cli-workers)
17
19
  - [Register A Custom Tool](#register-a-custom-tool)
18
20
  - [Use Policy And Approvals](#use-policy-and-approvals)
19
21
  - [Use Evidence And Claims](#use-evidence-and-claims)
@@ -77,18 +79,32 @@ import {
77
79
  EvidenceLedger,
78
80
  PolicyEngine,
79
81
  ToolGateway,
82
+ createAgentTool,
83
+ createCliAgentTool,
80
84
  createCrawlerTool,
81
85
  createResearchWorkflow
82
86
  } from "maqam";
83
87
 
84
88
  const evidenceLedger = new EvidenceLedger();
85
89
  const policyEngine = new PolicyEngine({
86
- allowedTools: ["crawler"],
90
+ allowedTools: ["crawler", "summarizer"],
87
91
  allowedOrigins: ["https://github.com"]
88
92
  });
89
93
 
90
94
  const toolGateway = new ToolGateway({ policyEngine, evidenceLedger });
91
95
  toolGateway.registerTool("crawler", createCrawlerTool());
96
+ toolGateway.registerTool("summarizer", createAgentTool(async (input) => ({
97
+ summary: `Reviewed ${input.topic}`
98
+ }), { name: "summarizer" }));
99
+ toolGateway.registerTool("localWorker", createCliAgentTool({
100
+ name: "localWorker",
101
+ command: process.execPath,
102
+ args: ["--version"],
103
+ stdin: "none",
104
+ timeoutMs: 5000,
105
+ maxInputTokens: 20,
106
+ maxOutputBytes: 2048
107
+ }));
92
108
 
93
109
  const runtime = new AgentRuntime({ policyEngine, evidenceLedger, toolGateway });
94
110
  const result = await runtime.runWorkflow(
@@ -98,7 +114,7 @@ const result = await runtime.runWorkflow(
98
114
  }),
99
115
  {
100
116
  objective: "Research Maqam from public sources",
101
- allowedTools: ["crawler"],
117
+ allowedTools: ["crawler", "summarizer"],
102
118
  allowedOrigins: ["https://github.com"],
103
119
  budget: { maxToolCalls: 20, maxRuntimeMs: 120_000 }
104
120
  }
@@ -210,6 +226,8 @@ import {
210
226
  PolicyEngine,
211
227
  ToolGateway,
212
228
  SkillRegistry,
229
+ createAgentTool,
230
+ createCliAgentTool,
213
231
  createCrawlerTool,
214
232
  createResearchWorkflow,
215
233
  crawl,
@@ -245,6 +263,8 @@ Core objects:
245
263
  - `AgentRuntime`: owns workflow execution.
246
264
  - `PolicyEngine`: decides what is allowed, denied, or approval-gated.
247
265
  - `ToolGateway`: routes all external tool calls through policy.
266
+ - `createAgentTool`: wraps arbitrary agents so they can be governed like any other tool.
267
+ - `createCliAgentTool`: wraps fixed command-line workers with timeout, approximate input-token limits, output byte limits, and no shell execution by default.
248
268
  - `EvidenceLedger`: stores source evidence and claim support.
249
269
  - `SkillRegistry`: stores skill metadata and selects matching skills.
250
270
  - `createResearchWorkflow`: bundled workflow for public web research.
@@ -560,6 +580,69 @@ await toolGateway.call("crawler", {
560
580
  });
561
581
  ```
562
582
 
583
+ ### `createAgentTool(agent, options)`
584
+
585
+ Wraps an arbitrary agent so it can be controlled by Maqam policy and executed through `ToolGateway`.
586
+
587
+ Supported agent shapes:
588
+
589
+ - Function agent: `async (input, context) => output`
590
+ - Object agent with `run(input, context)`
591
+ - Object agent with `invoke(input, context)`
592
+ - Object agent with `call(input, context)`
593
+
594
+ ```js
595
+ const summarizer = createAgentTool(async (input, context) => {
596
+ return {
597
+ summary: `Reviewed ${input.topic}`,
598
+ evidence: [
599
+ {
600
+ evidenceId: "ev_agent_1",
601
+ sourceType: "agent_output",
602
+ source: "summarizer",
603
+ excerpt: "The agent reviewed policy and evidence controls.",
604
+ confidence: 0.8
605
+ }
606
+ ],
607
+ claims: [
608
+ {
609
+ text: "The summarizer reviewed policy and evidence controls.",
610
+ evidenceIds: ["ev_agent_1"],
611
+ confidence: 0.8
612
+ }
613
+ ]
614
+ };
615
+ }, { name: "summarizer" });
616
+
617
+ toolGateway.registerTool("summarizer", summarizer);
618
+
619
+ const result = await toolGateway.call("summarizer", {
620
+ topic: "Maqam"
621
+ }, {
622
+ runId: "run_1",
623
+ taskId: "summarize"
624
+ });
625
+ ```
626
+
627
+ If the agent output includes `evidence` or `claims` arrays, Maqam records them into the active `EvidenceLedger`.
628
+
629
+ Object-agent example:
630
+
631
+ ```js
632
+ const browserAgent = {
633
+ async run(input) {
634
+ return {
635
+ url: input.url,
636
+ result: "Browser task completed"
637
+ };
638
+ }
639
+ };
640
+
641
+ toolGateway.registerTool("browserAgent", createAgentTool(browserAgent, {
642
+ name: "browserAgent"
643
+ }));
644
+ ```
645
+
563
646
  ### `createResearchWorkflow(options)`
564
647
 
565
648
  Creates the bundled public research workflow.
@@ -573,6 +656,93 @@ const workflow = createResearchWorkflow({
573
656
  });
574
657
  ```
575
658
 
659
+ ### `createCliAgentTool(options)`
660
+
661
+ Wraps a fixed command-line worker so it can run through Maqam policy and trace capture.
662
+
663
+ ```js
664
+ const localWorker = createCliAgentTool({
665
+ name: "localWorker",
666
+ command: process.execPath,
667
+ args: ["--input-type=module", "-e", "let body=''; for await (const c of process.stdin) body += c; const input = JSON.parse(body); console.log(JSON.stringify({ artifact: `built:${input.name}` }));"],
668
+ stdin: "json",
669
+ parseJson: true,
670
+ timeoutMs: 5000,
671
+ maxInputTokens: 50,
672
+ maxOutputBytes: 2048
673
+ });
674
+
675
+ toolGateway.registerTool("localWorker", localWorker);
676
+
677
+ const result = await toolGateway.call("localWorker", {
678
+ name: "demo-widget"
679
+ });
680
+
681
+ console.log(result.json.artifact);
682
+ ```
683
+
684
+ Options:
685
+
686
+ | Field | Type | Description |
687
+ | --- | --- | --- |
688
+ | `name` | `string` | Name used in result metadata. |
689
+ | `command` | `string` | Fixed executable path or command. Required. |
690
+ | `args` | `string[]` | Fixed argument list. Dynamic user input should go through stdin, not command args. |
691
+ | `cwd` | `string` | Optional working directory. |
692
+ | `env` | `object` | Extra environment variables. |
693
+ | `inheritEnv` | `boolean` | Inherit `process.env`. Default: `true`. |
694
+ | `stdin` | `"json" | "text" | "none"` | How input is passed to the worker. Default: `"json"`. |
695
+ | `parseJson` | `boolean` | Parse stdout as JSON and expose it as `result.json`. |
696
+ | `timeoutMs` | `number` | Hard runtime timeout. Default: `30000`. |
697
+ | `maxInputTokens` | `number` | Approximate input token limit. Default: `4000`. |
698
+ | `maxOutputBytes` | `number` | Maximum combined stdout/stderr bytes. Default: `65536`. |
699
+ | `rejectOnNonZero` | `boolean` | Reject when exit code is not zero. Default: `true`. |
700
+ | `shell` | `boolean` | Run through a shell. Default: `false`. Use only when a platform wrapper requires it. |
701
+
702
+ Result shape:
703
+
704
+ ```json
705
+ {
706
+ "name": "localWorker",
707
+ "command": "node",
708
+ "args": ["--version"],
709
+ "exitCode": 0,
710
+ "signal": null,
711
+ "timedOut": false,
712
+ "stdout": "v20.0.0\n",
713
+ "stderr": "",
714
+ "durationMs": 42,
715
+ "approxInputTokens": 0,
716
+ "outputBytes": 9,
717
+ "limits": {
718
+ "maxInputTokens": 50,
719
+ "maxOutputBytes": 2048,
720
+ "timeoutMs": 5000
721
+ }
722
+ }
723
+ ```
724
+
725
+ Limit errors:
726
+
727
+ | Code | Meaning |
728
+ | --- | --- |
729
+ | `CLI_INPUT_LIMIT_EXCEEDED` | Input was too large before execution. |
730
+ | `CLI_OUTPUT_LIMIT_EXCEEDED` | stdout/stderr exceeded the configured byte limit. |
731
+ | `CLI_TIMEOUT` | Process exceeded `timeoutMs`. |
732
+ | `CLI_EXIT_NONZERO` | Process exited with a non-zero code. |
733
+ | `CLI_JSON_PARSE_FAILED` | `parseJson` was enabled but stdout was not valid JSON. |
734
+ | `CLI_SPAWN_FAILED` | The process could not be started. |
735
+
736
+ Security notes:
737
+
738
+ - Maqam does not use a shell for CLI workers by default.
739
+ - Keep `command` and `args` fixed in code.
740
+ - Send user input through stdin.
741
+ - Use narrow `allowedTools`.
742
+ - Set short `timeoutMs` and small `maxOutputBytes` for untrusted workers.
743
+ - Use approval gates for workers that write, publish, send, or modify state.
744
+ - Prefer direct executable paths over platform wrapper scripts. On Windows, some `.cmd` or `.ps1` shims may require `shell: true` or a direct underlying executable path.
745
+
576
746
  Tasks:
577
747
 
578
748
  | Task ID | Purpose |
@@ -741,6 +911,187 @@ console.log(result.outputs.record_summary);
741
911
  console.log(result.evidence.unsupportedClaims);
742
912
  ```
743
913
 
914
+ ## Control Any Agent
915
+
916
+ Yes, Maqam can control agents beyond crawling. The pattern is:
917
+
918
+ 1. Wrap the agent with `createAgentTool`.
919
+ 2. Register it in `ToolGateway`.
920
+ 3. Put the agent name in `PolicyEngine.allowedTools`.
921
+ 4. Add it to `approvalRequiredTools` if it can write, publish, send, modify, or spend.
922
+ 5. Call it from an `AgentRuntime` workflow task.
923
+
924
+ Example with multiple agents:
925
+
926
+ ```js
927
+ import {
928
+ AgentRuntime,
929
+ EvidenceLedger,
930
+ PolicyEngine,
931
+ ToolGateway,
932
+ createAgentTool
933
+ } from "maqam";
934
+
935
+ const evidenceLedger = new EvidenceLedger();
936
+ const policyEngine = new PolicyEngine({
937
+ allowedTools: ["researchAgent", "reviewAgent", "publishAgent"],
938
+ approvalRequiredTools: ["publishAgent"]
939
+ });
940
+ const toolGateway = new ToolGateway({ policyEngine, evidenceLedger });
941
+
942
+ toolGateway.registerTool("researchAgent", createAgentTool(async (input) => ({
943
+ notes: `Researched ${input.topic}`,
944
+ evidence: [
945
+ {
946
+ evidenceId: "ev_research_1",
947
+ sourceType: "agent_output",
948
+ source: "researchAgent",
949
+ excerpt: `Researched ${input.topic}`,
950
+ confidence: 0.7
951
+ }
952
+ ]
953
+ }), { name: "researchAgent" }));
954
+
955
+ toolGateway.registerTool("reviewAgent", createAgentTool({
956
+ async run(input) {
957
+ return { approvedForDraft: Boolean(input.notes) };
958
+ }
959
+ }, { name: "reviewAgent" }));
960
+
961
+ toolGateway.registerTool("publishAgent", createAgentTool(async () => ({
962
+ published: true
963
+ }), { name: "publishAgent" }));
964
+
965
+ const workflow = {
966
+ name: "multi_agent_governed_flow",
967
+ tasks: [
968
+ {
969
+ id: "research",
970
+ run: (context) => context.tools.call("researchAgent", { topic: "Maqam" }, context)
971
+ },
972
+ {
973
+ id: "review",
974
+ run: (context) => context.tools.call("reviewAgent", context.outputs.research, context)
975
+ },
976
+ {
977
+ id: "publish",
978
+ run: (context) => context.tools.call("publishAgent", context.outputs.review, context)
979
+ }
980
+ ]
981
+ };
982
+
983
+ const runtime = new AgentRuntime({ policyEngine, evidenceLedger, toolGateway });
984
+ const result = await runtime.runWorkflow(workflow, {
985
+ objective: "Run a governed multi-agent workflow",
986
+ allowedTools: ["researchAgent", "reviewAgent", "publishAgent"]
987
+ });
988
+
989
+ console.log(result.status);
990
+ ```
991
+
992
+ In this example, `publishAgent` will throw `ApprovalRequiredError` because it is approval-gated. That is intentional: Maqam controls the agent rather than letting it publish directly.
993
+
994
+ What Maqam can control:
995
+
996
+ - Function agents.
997
+ - LangChain/LangGraph-style agents if exposed through `invoke` or wrapped in a function.
998
+ - External SDK-style functions if wrapped in a function.
999
+ - Browser agents.
1000
+ - Research agents.
1001
+ - GitHub/npm/internal API agents.
1002
+ - Email, Slack, Jira, database, or release agents when registered as tools.
1003
+
1004
+ What Maqam cannot do automatically:
1005
+
1006
+ - It cannot control an agent you do not route through `ToolGateway`.
1007
+ - It cannot make an unsafe third-party agent safe if that agent bypasses the wrapper and performs side effects internally.
1008
+ - It cannot approve risky actions by itself; approval-gated actions should be routed to humans.
1009
+
1010
+ ## Control CLI Workers
1011
+
1012
+ Maqam can govern command-line workers the same way it governs function agents.
1013
+
1014
+ The pattern is:
1015
+
1016
+ 1. Create a fixed CLI adapter with `createCliAgentTool`.
1017
+ 2. Register it with `ToolGateway`.
1018
+ 3. Add the worker name to `allowedTools`.
1019
+ 4. Configure `timeoutMs`, `maxInputTokens`, and `maxOutputBytes`.
1020
+ 5. Call the worker from a workflow task.
1021
+
1022
+ Example workflow:
1023
+
1024
+ ```js
1025
+ import {
1026
+ AgentRuntime,
1027
+ EvidenceLedger,
1028
+ PolicyEngine,
1029
+ ToolGateway,
1030
+ createCliAgentTool
1031
+ } from "maqam";
1032
+
1033
+ const evidenceLedger = new EvidenceLedger();
1034
+ const policyEngine = new PolicyEngine({
1035
+ allowedTools: ["builderWorker"]
1036
+ });
1037
+ const toolGateway = new ToolGateway({ policyEngine, evidenceLedger });
1038
+
1039
+ toolGateway.registerTool("builderWorker", createCliAgentTool({
1040
+ name: "builderWorker",
1041
+ command: process.execPath,
1042
+ args: ["--input-type=module", "-e", "let body=''; for await (const c of process.stdin) body += c; const input = JSON.parse(body); console.log(JSON.stringify({ fileName: `${input.name}.txt`, content: `Built ${input.name}` }));"],
1043
+ stdin: "json",
1044
+ parseJson: true,
1045
+ timeoutMs: 5000,
1046
+ maxInputTokens: 100,
1047
+ maxOutputBytes: 4096
1048
+ }));
1049
+
1050
+ const workflow = {
1051
+ name: "governed_cli_build",
1052
+ tasks: [
1053
+ {
1054
+ id: "build",
1055
+ run: (context) => context.tools.call("builderWorker", {
1056
+ name: "demo-widget"
1057
+ }, context)
1058
+ },
1059
+ {
1060
+ id: "record",
1061
+ run: (context) => {
1062
+ const output = context.outputs.build.json;
1063
+ const evidence = context.evidence.addEvidence({
1064
+ runId: context.runId,
1065
+ taskId: "record",
1066
+ sourceType: "cli_worker_output",
1067
+ source: "builderWorker",
1068
+ excerpt: output.content,
1069
+ tool: "builderWorker",
1070
+ confidence: 0.8
1071
+ });
1072
+ context.evidence.addClaim({
1073
+ runId: context.runId,
1074
+ taskId: "record",
1075
+ text: `The worker created ${output.fileName}.`,
1076
+ evidenceIds: [evidence.evidenceId],
1077
+ confidence: 0.8
1078
+ });
1079
+ return output;
1080
+ }
1081
+ }
1082
+ ]
1083
+ };
1084
+
1085
+ const runtime = new AgentRuntime({ policyEngine, evidenceLedger, toolGateway });
1086
+ const result = await runtime.runWorkflow(workflow, {
1087
+ objective: "Build a small artifact through a governed CLI worker",
1088
+ allowedTools: ["builderWorker"]
1089
+ });
1090
+
1091
+ console.log(result.outputs.record);
1092
+ console.log(result.evidence.unsupportedClaims);
1093
+ ```
1094
+
744
1095
  ## Register A Custom Tool
745
1096
 
746
1097
  Tools should be small and explicit. The gateway handles policy and trace capture.
@@ -1092,3 +1443,7 @@ Useful next packages or modules:
1092
1443
  - Browser automation connector.
1093
1444
  - GitHub and npm metadata connectors.
1094
1445
  - Tenant-aware configuration and audit export.
1446
+
1447
+ ## Open Development
1448
+
1449
+ Maqam is open source under MIT and open for development, issues, ideas, and contributions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maqam",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Maqam is an MIT-licensed Ajnas agent framework for governed workflows, policy, evidence, skills, and crawler-backed research.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -43,6 +43,8 @@
43
43
  "skills",
44
44
  "tool-orchestration",
45
45
  "human-approval",
46
+ "cli-agent",
47
+ "command-runner",
46
48
  "crawler",
47
49
  "agent",
48
50
  "web-crawler",
@@ -0,0 +1,43 @@
1
+ function resolveAgentInvoker(agent) {
2
+ if (typeof agent === "function") return agent;
3
+ if (agent && typeof agent.run === "function") return agent.run.bind(agent);
4
+ if (agent && typeof agent.invoke === "function") return agent.invoke.bind(agent);
5
+ if (agent && typeof agent.call === "function") return agent.call.bind(agent);
6
+ throw new TypeError("createAgentTool requires a function agent or an object with run, invoke, or call.");
7
+ }
8
+
9
+ function recordAgentEvidence(result, context, agentName) {
10
+ const ledger = context.evidenceLedger || context.evidence;
11
+ if (!ledger || !result || typeof result !== "object") return;
12
+
13
+ for (const item of result.evidence || []) {
14
+ ledger.addEvidence({
15
+ runId: context.runId || item.runId || null,
16
+ taskId: context.taskId || item.taskId || null,
17
+ tool: context.toolName || agentName,
18
+ ...item
19
+ });
20
+ }
21
+
22
+ for (const item of result.claims || []) {
23
+ ledger.addClaim({
24
+ runId: context.runId || item.runId || null,
25
+ taskId: context.taskId || item.taskId || null,
26
+ ...item
27
+ });
28
+ }
29
+ }
30
+
31
+ export function createAgentTool(agent, options = {}) {
32
+ const invoke = resolveAgentInvoker(agent);
33
+ const agentName = options.name || agent?.name || "agent";
34
+
35
+ return async function agentTool(input = {}, context = {}) {
36
+ const result = await invoke(input, {
37
+ ...context,
38
+ agentName
39
+ });
40
+ recordAgentEvidence(result, context, agentName);
41
+ return result;
42
+ };
43
+ }
@@ -0,0 +1,185 @@
1
+ import { spawn } from "node:child_process";
2
+ import { AjnasFrameworkError } from "./errors.js";
3
+
4
+ const DEFAULT_TIMEOUT_MS = 30_000;
5
+ const DEFAULT_MAX_INPUT_TOKENS = 4_000;
6
+ const DEFAULT_MAX_OUTPUT_BYTES = 64 * 1024;
7
+
8
+ function estimateTokens(value) {
9
+ return Math.ceil(Buffer.byteLength(value || "", "utf8") / 4);
10
+ }
11
+
12
+ function buildStdin(input, mode) {
13
+ if (mode === "none") return null;
14
+ if (mode === "text") return String(input.prompt ?? input.text ?? "");
15
+ return JSON.stringify(input);
16
+ }
17
+
18
+ function cliError(message, code, details = {}) {
19
+ return new AjnasFrameworkError(message, {
20
+ code,
21
+ details
22
+ });
23
+ }
24
+
25
+ export function createCliAgentTool(options = {}) {
26
+ const {
27
+ name = "cliAgent",
28
+ command,
29
+ args = [],
30
+ cwd,
31
+ env = {},
32
+ inheritEnv = true,
33
+ stdin = "json",
34
+ parseJson = false,
35
+ timeoutMs = DEFAULT_TIMEOUT_MS,
36
+ maxInputTokens = DEFAULT_MAX_INPUT_TOKENS,
37
+ maxOutputBytes = DEFAULT_MAX_OUTPUT_BYTES,
38
+ rejectOnNonZero = true,
39
+ shell = false
40
+ } = options;
41
+
42
+ if (!command || typeof command !== "string") {
43
+ throw new TypeError("createCliAgentTool requires a fixed command string.");
44
+ }
45
+ if (!Array.isArray(args) || args.some((arg) => typeof arg !== "string")) {
46
+ throw new TypeError("createCliAgentTool args must be an array of strings.");
47
+ }
48
+
49
+ return async function cliAgentTool(input = {}) {
50
+ const stdinBody = buildStdin(input, stdin);
51
+ const approxInputTokens = estimateTokens(stdinBody || "");
52
+ if (maxInputTokens && approxInputTokens > maxInputTokens) {
53
+ throw cliError(`CLI input exceeds maxInputTokens (${approxInputTokens} > ${maxInputTokens}).`, "CLI_INPUT_LIMIT_EXCEEDED", {
54
+ name,
55
+ approxInputTokens,
56
+ maxInputTokens
57
+ });
58
+ }
59
+
60
+ return new Promise((resolve, reject) => {
61
+ const startedAt = Date.now();
62
+ let child;
63
+ try {
64
+ child = spawn(command, args, {
65
+ cwd,
66
+ env: inheritEnv ? { ...process.env, ...env } : { ...env },
67
+ shell,
68
+ windowsHide: true,
69
+ stdio: ["pipe", "pipe", "pipe"]
70
+ });
71
+ } catch (error) {
72
+ reject(cliError(error.message, "CLI_SPAWN_FAILED", {
73
+ name,
74
+ command,
75
+ cause: error.code || error.name
76
+ }));
77
+ return;
78
+ }
79
+
80
+ const stdout = [];
81
+ const stderr = [];
82
+ let outputBytes = 0;
83
+ let settled = false;
84
+
85
+ const finish = (callback, value) => {
86
+ if (settled) return;
87
+ settled = true;
88
+ clearTimeout(timer);
89
+ callback(value);
90
+ };
91
+
92
+ const stopWithError = (error) => {
93
+ if (!child.killed) child.kill();
94
+ finish(reject, error);
95
+ };
96
+
97
+ const timer = setTimeout(() => {
98
+ stopWithError(cliError(`CLI agent '${name}' timed out after ${timeoutMs}ms.`, "CLI_TIMEOUT", {
99
+ name,
100
+ timeoutMs
101
+ }));
102
+ }, timeoutMs);
103
+
104
+ const collect = (target, chunk) => {
105
+ outputBytes += chunk.byteLength;
106
+ if (outputBytes > maxOutputBytes) {
107
+ stopWithError(cliError(`CLI output exceeds maxOutputBytes (${outputBytes} > ${maxOutputBytes}).`, "CLI_OUTPUT_LIMIT_EXCEEDED", {
108
+ name,
109
+ maxOutputBytes,
110
+ outputBytes
111
+ }));
112
+ return;
113
+ }
114
+ target.push(Buffer.from(chunk));
115
+ };
116
+
117
+ child.stdout.on("data", (chunk) => collect(stdout, chunk));
118
+ child.stderr.on("data", (chunk) => collect(stderr, chunk));
119
+
120
+ child.on("error", (error) => {
121
+ finish(reject, cliError(error.message, "CLI_SPAWN_FAILED", {
122
+ name,
123
+ command,
124
+ cause: error.code || error.name
125
+ }));
126
+ });
127
+
128
+ child.on("close", (exitCode, signal) => {
129
+ if (settled) return;
130
+
131
+ const stdoutText = Buffer.concat(stdout).toString("utf8");
132
+ const stderrText = Buffer.concat(stderr).toString("utf8");
133
+ const result = {
134
+ name,
135
+ command,
136
+ args,
137
+ exitCode,
138
+ signal,
139
+ timedOut: false,
140
+ stdout: stdoutText,
141
+ stderr: stderrText,
142
+ durationMs: Date.now() - startedAt,
143
+ approxInputTokens,
144
+ outputBytes,
145
+ limits: {
146
+ maxInputTokens,
147
+ maxOutputBytes,
148
+ timeoutMs
149
+ }
150
+ };
151
+
152
+ if (parseJson && stdoutText.trim()) {
153
+ try {
154
+ result.json = JSON.parse(stdoutText.trim());
155
+ } catch (error) {
156
+ finish(reject, cliError("CLI stdout was not valid JSON.", "CLI_JSON_PARSE_FAILED", {
157
+ name,
158
+ message: error.message
159
+ }));
160
+ return;
161
+ }
162
+ }
163
+
164
+ if (rejectOnNonZero && exitCode !== 0) {
165
+ finish(reject, cliError(`CLI agent '${name}' exited with code ${exitCode}.`, "CLI_EXIT_NONZERO", {
166
+ ...result,
167
+ stdout: stdoutText.slice(0, 2048),
168
+ stderr: stderrText.slice(0, 2048)
169
+ }));
170
+ return;
171
+ }
172
+
173
+ finish(resolve, result);
174
+ });
175
+
176
+ if (stdinBody === null) {
177
+ child.stdin.end();
178
+ } else {
179
+ child.stdin.end(stdinBody);
180
+ }
181
+ });
182
+ };
183
+ }
184
+
185
+ export { estimateTokens as estimateCliInputTokens };
package/src/index.js CHANGED
@@ -339,6 +339,8 @@ export { EvidenceLedger } from "./framework/evidence-ledger.js";
339
339
  export { ToolGateway } from "./framework/tool-gateway.js";
340
340
  export { SkillRegistry } from "./framework/skill-registry.js";
341
341
  export { AgentRuntime } from "./framework/runtime.js";
342
+ export { createAgentTool } from "./framework/agent-tool.js";
343
+ export { createCliAgentTool, estimateCliInputTokens } from "./framework/cli-agent-tool.js";
342
344
  export { createResearchWorkflow } from "./framework/research-workflow.js";
343
345
 
344
346
  export function createCrawlerTool(defaultOptions = {}) {