diff --git a/application/single_app/config.py b/application/single_app/config.py index a3413ac05..17903e612 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -101,7 +101,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.261.251" +VERSION = "0.261.252" IS_DEVELOPMENT = is_development_env_enabled() # Opt-out for deployments where App Service Easy Auth is active but the platform diff --git a/application/single_app/functions_workflow_result_reader.py b/application/single_app/functions_workflow_result_reader.py index f34cf5939..bfb4b4b3a 100644 --- a/application/single_app/functions_workflow_result_reader.py +++ b/application/single_app/functions_workflow_result_reader.py @@ -32,7 +32,7 @@ from functions_analysis_access import AnalysisResultUnavailable from functions_appinsights import log_event from functions_workflow_handoff_builder import HANDOFF_REPORT_NODE_ID, HANDOFF_REPORT_OUTPUT -from functions_workflow_node_results import load_node_result, result_selectors +from functions_workflow_node_results import authorize_workflow_node_result_read, load_node_result, result_selectors from functions_workflow_result_masking import WORKFLOW_RESULT_VERSION from functions_workflow_result_store import ( MAX_PAGE_BYTES, @@ -388,7 +388,7 @@ def _handoff_receipt(workflow, run, workflow_id, user_id): return receipt -def _read_handoff_result(workflow, run, workflow_id, run_id, receipt, *, expected_sha256, +def _read_handoff_result(workflow, run, workflow_id, run_id, receipt, *, user_id, expected_sha256, include_excerpts, budget, load_result, read_page): """Describe (or excerpt) a hand-off's report, the workflow's one output, by its exact selectors.""" status = run.get("status") @@ -416,6 +416,12 @@ def _read_handoff_result(workflow, run, workflow_id, run_id, receipt, *, expecte ): raise _closed("workflow_result_invalid", "manifest") _guarded("manifest", lambda: _require_completed_result(manifest, allow_partial=allow_partial)) + # Re-prove the report's lineage against the saved flow, as the general path does for every + # task row: each consumed-input receipt must chain to a real parent result of this run. + _guarded("authorize", lambda: authorize_workflow_node_result_read( + workflow, run_id, producer, receipt["result_ref"], reader_user_id=user_id, + manifest=manifest, load_result=loader, include_sources=False, + )) result = { "descriptor": _descriptor(workflow, run, workflow_id, run_id, digest), "partial": allow_partial, "output_count": 1, "excerpts": [], "saved_inputs": [], "truncated": False, @@ -819,7 +825,7 @@ def read_workflow_result( if receipt is None: raise _closed("workflow_result_unsupported", "run") return _read_handoff_result( - workflow, run, workflow_id, run_id, receipt, expected_sha256=expected_sha256, + workflow, run, workflow_id, run_id, receipt, user_id=user_id, expected_sha256=expected_sha256, include_excerpts=include_excerpts, budget=excerpt_budget_bytes, load_result=load_result, read_page=read_page, ) diff --git a/docs/explanation/features/CHAT_ORCHESTRATION_WORKFLOW_HANDOFF.md b/docs/explanation/features/CHAT_ORCHESTRATION_WORKFLOW_HANDOFF.md index 75d602d59..3786a826e 100644 --- a/docs/explanation/features/CHAT_ORCHESTRATION_WORKFLOW_HANDOFF.md +++ b/docs/explanation/features/CHAT_ORCHESTRATION_WORKFLOW_HANDOFF.md @@ -564,11 +564,26 @@ hand-off run only when all of these hold: - The run id is the one the hand-off derives. - The run has exactly one output, the report node's text, with a well-formed SHA-256 result reference. -- The node lineage still matches the saved flow. An edited or re-enabled - workflow fails closed. -Anything else is `workflow_result_unsupported`. Excerpts are bounded and carry -no references or ids, and a large report is paged once. +Anything else is `workflow_result_unsupported`. Before it describes or excerpts +the report, the reader checks the report itself: + +- The report's node identity still matches the saved flow. An edited or + re-enabled workflow fails closed with `workflow_result_invalid` before + anything is loaded. +- The report's lineage is re-proved with the shared node lineage authorizer, + the same walk the general path runs for every task row. Each consumed-input + receipt must chain to a real parent result of this run, with matching hashes + and references. A receipt that doesn't chain, or a damaged parent, is + `workflow_result_invalid`, and a missing parent is + `workflow_result_not_found`. The walk uses the saved flow and never + re-resolves sources. + +Both checks apply to a descriptor-only read, such as re-checking a stored chat +context, as well as to an excerpt read. Excerpts are bounded and carry no +references or ids, and a large report is paged once. The lineage re-proof was +added in 0.261.252; see the +[Workflow hand-off result lineage fix](../fixes/WORKFLOW_HANDOFF_RESULT_LINEAGE_FIX.md). A cancelled hand-off posts its note once. A paused hand-off that's never resumed gets the expired notice when its delivery window, the run's deadline @@ -654,7 +669,7 @@ All paths are under `application/single_app/`. | `functions_workflow_drafts.py` | The blueprint schema and checks, the hand-off dry run and create, and edited payload checks. | | `functions_workflow_definitions.py`, `functions_workflow_definition_store.py` | `origin.one_time` as a server-only field, and `one_time_status` outside the revision. | | `functions_workflow_runtime.py` | Recording `one_time_status` when the hand-off's run ends. | -| `functions_workflow_result_reader.py` | Opening only a hand-off's own report. | +| `functions_workflow_result_reader.py` | Opening only a hand-off's own report, after re-proving its lineage. | | `functions_workflow_limits.py` | The hand-off limits. | | `functions_personal_workflows.py` | Counting recent hand-offs, and leaving them out of the workflows-per-user count. | | `functions_orchestration_workflow_context.py` | The gates and the hand-off planning context. | @@ -716,7 +731,7 @@ See [Orchestration settings](../../admin/orchestration.md). | `functional_tests/test_workflow_handoff_builder.py` | The v3 For each, the disclosure, every refusal, ids that never match a proposal's, creating at most once, and edited payloads building what a save builds. | | `functional_tests/test_workflow_handoff_origin.py` | `origin.one_time` as a server-only field on every client path, and hand-off and proposal creates never adopting each other's workflow. | | `functional_tests/test_workflow_handoff_lifecycle.py` | `one_time_status` on every terminal transition, the over-limit pause, and cancelled and expired hand-offs in chat. | -| `functional_tests/test_workflow_handoff_result_reader.py` | The reader opening only a hand-off's own report, lineage, digests and paging. | +| `functional_tests/test_workflow_handoff_result_reader.py` | The reader opening only a hand-off's own report; re-proving its lineage against a real parent result, and refusing a malformed receipt, a missing or damaged parent, or an edited definition before the report's text is read; digests and paging. | | `functional_tests/test_workflow_handoff_end_to_end.py` | 3-document and 200-document workspace queries through the real durable runtime and runner, ending with one summary posted to chat and no private values in any response or log. | | `functional_tests/route_tests/test_route_blueprint_policy_inventory.py`, `functional_tests/route_tests/test_route_unauthenticated_policy_contract.py` | The four routes in the route policy. | @@ -728,7 +743,9 @@ workspaces in scope, at most 100. The step dry-runs once. An accept rebuilds the workflow, counts the user's recent hand-offs with one query, creates one workflow and queues one run. The status route reads each hand-off's workflow, and its run once queued. The report reads saved findings in pages; the -200-document test checks pages of at most 100 records and 128 KiB. +200-document test checks pages of at most 100 records and 128 KiB. Each read of +the finished report walks its lineage again, about seven result-store loads per +document; the 200-document read makes 1,409. ### Known limitations diff --git a/docs/explanation/fixes/WORKFLOW_HANDOFF_RESULT_LINEAGE_FIX.md b/docs/explanation/fixes/WORKFLOW_HANDOFF_RESULT_LINEAGE_FIX.md new file mode 100644 index 000000000..2e30cd050 --- /dev/null +++ b/docs/explanation/fixes/WORKFLOW_HANDOFF_RESULT_LINEAGE_FIX.md @@ -0,0 +1,204 @@ +# Workflow Hand-off Result Lineage Fix + +**Version: 0.261.252** + +Fixed in version: **0.261.252**, recorded in +`application/single_app/config.py`. + +This affects the React V2 branch (`paullizer-react-v2-ui`) and deployments built +from it. It follows up +[#1640](https://github.com/microsoft/simplechat/pull/1640), which added workflow +hand-off from chat, and is part of +[#1549](https://github.com/microsoft/simplechat/issues/1549) and +[#1543](https://github.com/microsoft/simplechat/issues/1543). Hand-off is behind +the **Hand Off Large Work From Chat** setting +(`enable_chat_orchestration_workflow_handoff`), which is off by default. + +## Issue + +The workflow result reader read a chat hand-off's report without re-proving +where the report's inputs came from. + +When a hand-off run finishes, workflow result delivery reads the run's one +output, the report node's text, and posts it into the conversation. The reader +checked the report's stored result: its node identity against the saved +workflow, its text output against the run's receipt, and the completion rule. +It never checked the report's consumed-input receipts, which name the parent +results the report was written from. + +So a stored report whose receipts didn't chain to real results of the run still +read as available. A report saved again with `consumed_inputs` set to +`[{"malformed_receipt": true}]`, under its own valid hash, read with +`available: true` and its full text. The shared node lineage authorizer refuses +the same result with `analysis_lineage_invalid`. A missing or damaged parent +result didn't stop the read either. + +The reader's general path, for runs that aren't structured, walks this lineage +for every task row before it reads any result. The hand-off feature doc said the +hand-off read did the same. + +## Root cause + +Phase 7a added `_read_handoff_result()` to +`functions_workflow_result_reader.py`, a narrow path that opens only a hand-off +run's report. It loads the report's manifest with `load_node_result()`, which +recomputes the node identity from the saved workflow, so an edited or +re-enabled workflow already failed closed before any load. The general path's +lineage check, `authorize_workflow_run_read()`, walks every task row, and the +hand-off path didn't replace it with a check of its own report. Nothing walked +the report's lineage. + +## Changes + +### The hand-off read walks the report's lineage + +After the manifest, identity, output and completion checks, and before the +result is described or excerpted, `_read_handoff_result()` now calls the shared +node lineage authorizer: + +```python +_guarded("authorize", lambda: authorize_workflow_node_result_read( + workflow, run_id, producer, receipt["result_ref"], reader_user_id=user_id, + manifest=manifest, load_result=loader, include_sources=False, +)) +``` + +- The walk proves that each result belongs to this workflow and run, that its + hashes and references match, and that each consumed-input receipt chains to a + real parent result. It also covers selected producers, iteration paths, repeat + state and frozen loops. +- It walks the saved workflow, the same definition `load_node_result()` checks + the report's identity against. An edited or re-enabled workflow still fails + closed before any load. +- It runs for the descriptor-only read and the excerpt read alike. A stored chat + context that refers to the report is read the descriptor-only way, so it's + re-proved too. +- It reuses the reader's manifest memo and the report manifest the reader has + already loaded. +- `include_sources=False` skips keeping the list of contributing sources, which + the reader doesn't use. The walk never re-resolves sources either way. +- `read_workflow_result()` passes its `user_id`, which it has already matched to + the workflow's owner. + +Failures are logged at stage `authorize` and map to the same codes the general +path uses: + +| Failure | Code | +| --- | --- | +| A receipt that doesn't chain, or a parent whose output disagrees with the receipt | `workflow_result_invalid` | +| A parent whose stored bytes don't match its hash | `workflow_result_invalid` | +| A missing parent result | `workflow_result_not_found` (404) | + +The authorizer itself didn't change. + +### Recaptured hand-off off-golden + +`functional_tests/test_orchestration_workflow_handoff_off_golden.py` checks that +planning is byte-identical when hand-off is unavailable. On V2 after #1640 +merged, 4 of its 10 cases failed: the `absent`, `false`, `on` and `string` cases +of `test_planning_is_byte_identical_when_handoff_is_unavailable`. + +Its fixture was captured on f1aeef13d (0.261.233), before +[#1641](https://github.com/microsoft/simplechat/pull/1641) added file merging +and changed the planner's content. The Phase 7a branch's last commit, +5a625599c, recaptured the fixture with `--write-golden` on unmodified V2 +a5a5b1c5 (0.261.248), but it wasn't pushed before #1640 merged. + +This fix cherry-picks that commit unchanged with `git cherry-pick -x`. It changes +only the test's `CAPTURED_FROM` value and docstring line, and the JSON fixture, +whose blob is `f93f8e4c3dcc263cc5a71186f2cecc8c60574098`. A separate recapture +on a fresh worktree at a5a5b1c5 produced the same bytes. The test's +regeneration instructions didn't change. + +## Files modified + +| File | Change | +| --- | --- | +| `application/single_app/functions_workflow_result_reader.py` | The hand-off read walks the report's lineage. | +| `functional_tests/test_workflow_handoff_result_reader.py` | A fixture with real lineage, and the tests below. | +| `functional_tests/test_orchestration_workflow_handoff_off_golden.py`, `functional_tests/test_support/orchestration_workflow_handoff_off_golden.json` | The recaptured fixture. | +| `functional_tests/test_workflow_handoff_builder.py` | The Phase 4 schema digest pin, refreshed to the a5a5b1c53 value. | +| `docs/explanation/features/CHAT_ORCHESTRATION_WORKFLOW_HANDOFF.md` | The reader's checks, the performance note, and the files and test tables. | +| `docs/explanation/release_notes.md` | 0.261.252. | +| `application/single_app/config.py` | Version `0.261.252`. | + +## Validation + +### Tests + +`functional_tests/test_workflow_handoff_result_reader.py` uses the real hand-off +builder, result contract, node identity, lineage authorizer and reader, with +fake containers and an in-memory result store. Nothing on the lineage path is +stubbed. The new tests: + +- A report with a malformed consumed-input receipt is refused with + `workflow_result_invalid` for the excerpt read, the descriptor-only read and a + stored chat context. Only the report's manifest is loaded; its text and pages + never are. With the walk removed, the same report reads as available. +- A report with real lineage reads. Its parent is a real `collect` node result, + built and saved through the runner's result contract, and its receipt comes + from opening that result as the report's `findings` input. The read loads the + report's manifest, the parent, then the report's text, and returns the exact + report text. With the walk removed, the result is identical and only the + parent's load disappears, so the walk is really exercised. +- A descriptor-only read of that report loads its manifest and the parent, and no + section or page. +- A missing parent is refused with `workflow_result_not_found` (404) at stage + `authorize`. The general path returns the same code, status and stage for the + same failure. +- A parent whose output reference disagrees with the receipt, from either side, + is refused with `workflow_result_invalid`. So is a parent whose stored bytes + don't match its hash, checked with the result store's own hash verification. +- On the fixture with real lineage, a definition edit, turning the workflow on, + an alert change or a Run as change is refused before any load, as on the + original fixture. + +Each of these deliberate breaks makes tests fail, except one equivalent change: + +| Break | Result | +| --- | --- | +| No lineage walk | 7 tests fail | +| Walk only for excerpt reads | 6 tests fail, the descriptor-only refusals | +| `include_sources=True` | No test fails. The change is equivalent: the reader discards the authorizer's access summary. | +| Failures logged at stage `manifest` | 5 tests fail, on the logged stage | + +`functional_tests/test_workflow_handoff_end_to_end.py` runs 3-document and +200-document hand-offs through the real durable runtime, runner and reader. A +temporary probe, not committed, confirmed that each report read walks the +lineage once and the walk succeeds. With the walk removed, the 3-document read +drops from 27 node-result loads to 2, and the 200-document read from 1,409 to +2. + +### Phase 4 digest pin + +`functional_tests/test_workflow_handoff_builder.py` pins digests of the Phase 4 +blueprint schema, payloads, validation and dry run, to prove hand-off left them +unchanged. Its `test_the_phase_4_blueprint_and_payloads_are_unchanged` failed at +15feec650 because the schema pin predated +[#1641](https://github.com/microsoft/simplechat/pull/1641), which added the +`merge` task schema. The pin now holds the value V2 computes at a5a5b1c53, after +#1641 and before hand-off merged, so the test still proves hand-off didn't change +the Phase 4 schema. The other four pins are unchanged. + +### Before and after + +| Before | After | +| --- | --- | +| A report whose consumed-input receipts don't chain to the run's results is read and posted to chat. | It's refused with `workflow_result_invalid` before its text is loaded. | +| A missing or damaged parent result doesn't stop the read. | A missing parent is `workflow_result_not_found`; a damaged one is `workflow_result_invalid`. | +| A hand-off's report read loads the report's manifest and text. | It also walks the report's lineage. The 200-document read makes 1,409 node-result loads, about 1.7 seconds with the in-memory store. | +| The hand-off off-golden failed 4 of 10 cases. | It passes 10 of 10 under pytest, optimized pytest, and as a normal or optimized script. | + +## Limitations + +- Every read walks the report's lineage again; nothing is cached between reads, + as on the general path. Each load is a separate read from the result store, + about seven per document in the 200-document test. +- The walk doesn't check a report's consumed inputs against the inputs the + compiled flow declares. That's how the shared authorizer works, and it's + unchanged. + +## Related + +- [Chat orchestration workflow hand-off](../features/CHAT_ORCHESTRATION_WORKFLOW_HANDOFF.md#result-reading-and-delivery) +- [Workflow saved result source re-check fix](WORKFLOW_SAVED_RESULT_SOURCE_RECHECK_FIX.md) diff --git a/docs/explanation/release_notes.md b/docs/explanation/release_notes.md index c488a460f..c240209dd 100644 --- a/docs/explanation/release_notes.md +++ b/docs/explanation/release_notes.md @@ -2,6 +2,21 @@ For feature-focused and fix-focused drill-downs by version, see [Features by Version](https://github.com/microsoft/simplechat/tree/main/docs/explanation/features) and [Fixes by Version](https://github.com/microsoft/simplechat/tree/main/docs/explanation/fixes). +### **(v0.261.252)** + +#### Bug Fixes + +* **Workflow Hand-Off Reports Re-Prove Their Lineage** + * The workflow result reader now re-proves a chat hand-off's report with the shared node lineage authorizer before it describes or excerpts it, as the reader's general path does for each task row. Each consumed-input receipt must chain to a real parent result of the run, with matching hashes and references, so a report whose inputs don't check out is no longer posted to chat. + * A malformed receipt or a damaged parent result is refused with `workflow_result_invalid`, and a missing parent with `workflow_result_not_found`, the same codes the general path uses. The check runs for descriptor-only reads, such as re-checking a stored chat context, as well as excerpt reads. It walks the saved workflow, so an edited or re-enabled workflow still fails closed before anything is read. + * Hand-off stays off by default, behind **Hand Off Large Work From Chat**. + * (Ref: #1549, #1543, follow-up to #1640, `functions_workflow_result_reader.py`, `authorize_workflow_node_result_read`, `test_workflow_handoff_result_reader.py`, [Workflow Hand-off Result Lineage Fix](fixes/WORKFLOW_HANDOFF_RESULT_LINEAGE_FIX.md)) + +* **Hand-Off Off-Golden Fixture Recaptured** + * The fixture behind `test_orchestration_workflow_handoff_off_golden.py`, which checks that planning is unchanged when hand-off is unavailable, was captured on 0.261.233, before file merging (#1641) changed the planner's content, so 4 of its 10 cases failed on V2. + * It's now recaptured on unmodified V2 at 0.261.248, taken unchanged from the Phase 7a branch's last commit, and the test passes 10 of 10. Planning itself didn't change. + * (Ref: #1640, #1641, `test_orchestration_workflow_handoff_off_golden.py`, `test_support/orchestration_workflow_handoff_off_golden.json`, [Workflow Hand-off Result Lineage Fix](fixes/WORKFLOW_HANDOFF_RESULT_LINEAGE_FIX.md#recaptured-hand-off-off-golden)) + ### **(v0.261.251)** #### New Features diff --git a/functional_tests/test_orchestration_workflow_handoff_off_golden.py b/functional_tests/test_orchestration_workflow_handoff_off_golden.py index 7e26cc4f1..fd8188fb3 100644 --- a/functional_tests/test_orchestration_workflow_handoff_off_golden.py +++ b/functional_tests/test_orchestration_workflow_handoff_off_golden.py @@ -28,7 +28,7 @@ so it lists ``workflow_handoff`` for a missing role or a shared conversation exactly as it lists ``workflow_propose``; that one entry is checked, then left out of the comparison. -The fixture was captured on the unmodified base (f1aeef13d, 0.261.233, before workflow hand-off). +The fixture was captured on the unmodified base (a5a5b1c5, 0.261.248, before workflow hand-off). Never regenerate the fixture to make this test pass. After merging an upstream planner or route change, regenerate it from the upstream commit itself (without this feature): @@ -111,7 +111,7 @@ GOLDEN = Path(__file__).resolve().parent / "test_support" / "orchestration_workflow_handoff_off_golden.json" -CAPTURED_FROM = "f1aeef13d (0.261.233, before workflow hand-off)" +CAPTURED_FROM = "a5a5b1c5 (0.261.248, before workflow hand-off)" HANDOFF = "enable_chat_orchestration_workflow_handoff" RESULTS = "enable_chat_workflow_results" HANDOFF_CAPABILITY = "workflow_handoff" diff --git a/functional_tests/test_support/orchestration_workflow_handoff_off_golden.json b/functional_tests/test_support/orchestration_workflow_handoff_off_golden.json index 452cdfed2..f93f8e4c3 100644 --- a/functional_tests/test_support/orchestration_workflow_handoff_off_golden.json +++ b/functional_tests/test_support/orchestration_workflow_handoff_off_golden.json @@ -1,5 +1,5 @@ { - "captured_from": "f1aeef13d (0.261.233, before workflow hand-off)", + "captured_from": "a5a5b1c5 (0.261.248, before workflow hand-off)", "system_prompts": { "prompt_1": [ "Plan bounded work; do not execute or answer it. Return one JSON object.", @@ -83,7 +83,11 @@ "invent one. If the user already selected documents, plan around those documents.", "Use each candidate's server-resolved source_kind, not its display label, to choose compatible", "work. Never send tabular source IDs to document_analyze or document_compare: those steps", - "do not admit native tabular inputs. For mixed narrative/tabular comparisons, prepare each", + "do not admit native tabular inputs. To combine the rows of several tabular sources into one", + "table or file, use tabular_merge with every one of those sources, never compose; it appends", + "rows and cannot match rows on a key, so ask when the user may mean that. When their columns", + "may differ, choose union, aliases or exclusion, or inspect them with tabular_inspect first.", + "For mixed narrative/tabular comparisons, prepare each", "source with compatible offered capabilities and compose their named results. Ask each", "preparation step only for its own sources' contribution, such as values, periods, units and", "identifiers. A preparation step sees only its own sources, so never ask it to compare them", @@ -239,8 +243,10 @@ "user's request, without replacing the original \"message\" or user selections. Plan around", "accepted source identities and explanations, including on repeated questions. Do not repeat a", "question that clarifications or earlier runs already answered or declined. Only ask when you", - "truly cannot proceed; a reasonable assumption about what the user means, stated in", - "\"assumptions\", is better than a question.", + "truly cannot proceed, or when a capability's when_to_use, a fact in", + "capability_availability.deliverables or the workflow instructions say to ask; otherwise a", + "reasonable assumption about what the user means, stated in \"assumptions\", is better than a", + "question.", "", "", "Workflows. \"workflow_planning\" is present because this user may be offered one personal", @@ -278,6 +284,26 @@ " words such as \"this week\" can stay relative.", "- optionally \"alerts\" {\"mode\":\"every_run\"|\"failures_only\",\"severity\":\"info\"|\"low\"}, \"run_as\"", " \"self\"|\"none\", and \"durable\": true.", + "- A task can merge files with code instead of a model by adding \"merge\": {\"kind\",\"files\",", + " \"output_format\",\"file_name\",\"options\"} and no agent runner. kind \"tabular\" (the default) appends", + " the rows of CSV and Excel files into one \"csv\" or \"xlsx\" file; \"workbook\" puts each CSV or Excel", + " file on its own sheet of one \"xlsx\" workbook; \"pdf\" joins PDFs into one \"pdf\"; \"docx\" appends", + " Word documents into one \"docx\"; \"pptx\" appends the slides of PowerPoint decks into one \"pptx\".", + " When the user wants spreadsheets merged into one Excel file, or names no format, and has not", + " said whether every row goes on one sheet (kind \"tabular\" with \"xlsx\") or each file keeps its", + " own sheet (kind \"workbook\"), ask before proposing: one required single-choice string enum of", + " exactly \"All rows on one sheet\" and \"Each file on its own sheet\". \"csv\" output always means", + " one sheet.", + " files \"inputs\"", + " merges the task's two or more input documents in order; \"changed\" merges the files a file_sync", + " trigger added or changed; \"all\" merges every matching file in the user's personal workspace;", + " \"recent\" merges those added or changed in the last \"recent_window_minutes\". For tabular,", + " \"options\" takes tabular_merge's column, sheet, duplicate and sort settings, with", + " \"column_aliases\" written as [{\"column\",\"aliases\":[...]}]; workbook takes \"sheets\" and \"sheet\";", + " pdf takes \"bookmarks\"; docx takes \"formatting\", \"page_breaks\" and \"source_headings\"; pptx takes", + " \"formatting\" and \"sections\". Propose one when the user wants files merged on a schedule or on", + " every sync, or more files than one chat merge allows; a one-time merge of a few files runs in", + " chat with tabular_merge or document_merge. It needs no task_actions.", "Refer to agents, documents and File Sync sources only by their workflow_planning.catalog handles,", "and never write a record id, model or endpoint into a blueprint. \"task_actions\" lists, for each task", "in order, the action kinds it needs from the capability's input schema, or [] when it needs none.", @@ -411,7 +437,11 @@ "invent one. If the user already selected documents, plan around those documents.", "Use each candidate's server-resolved source_kind, not its display label, to choose compatible", "work. Never send tabular source IDs to document_analyze or document_compare: those steps", - "do not admit native tabular inputs. For mixed narrative/tabular comparisons, prepare each", + "do not admit native tabular inputs. To combine the rows of several tabular sources into one", + "table or file, use tabular_merge with every one of those sources, never compose; it appends", + "rows and cannot match rows on a key, so ask when the user may mean that. When their columns", + "may differ, choose union, aliases or exclusion, or inspect them with tabular_inspect first.", + "For mixed narrative/tabular comparisons, prepare each", "source with compatible offered capabilities and compose their named results. Ask each", "preparation step only for its own sources' contribution, such as values, periods, units and", "identifiers. A preparation step sees only its own sources, so never ask it to compare them", @@ -567,8 +597,10 @@ "user's request, without replacing the original \"message\" or user selections. Plan around", "accepted source identities and explanations, including on repeated questions. Do not repeat a", "question that clarifications or earlier runs already answered or declined. Only ask when you", - "truly cannot proceed; a reasonable assumption about what the user means, stated in", - "\"assumptions\", is better than a question.", + "truly cannot proceed, or when a capability's when_to_use, a fact in", + "capability_availability.deliverables or the workflow instructions say to ask; otherwise a", + "reasonable assumption about what the user means, stated in \"assumptions\", is better than a", + "question.", "" ], "prompt_3": [ @@ -653,7 +685,11 @@ "invent one. If the user already selected documents, plan around those documents.", "Use each candidate's server-resolved source_kind, not its display label, to choose compatible", "work. Never send tabular source IDs to document_analyze or document_compare: those steps", - "do not admit native tabular inputs. For mixed narrative/tabular comparisons, prepare each", + "do not admit native tabular inputs. To combine the rows of several tabular sources into one", + "table or file, use tabular_merge with every one of those sources, never compose; it appends", + "rows and cannot match rows on a key, so ask when the user may mean that. When their columns", + "may differ, choose union, aliases or exclusion, or inspect them with tabular_inspect first.", + "For mixed narrative/tabular comparisons, prepare each", "source with compatible offered capabilities and compose their named results. Ask each", "preparation step only for its own sources' contribution, such as values, periods, units and", "identifiers. A preparation step sees only its own sources, so never ask it to compare them", @@ -809,8 +845,10 @@ "user's request, without replacing the original \"message\" or user selections. Plan around", "accepted source identities and explanations, including on repeated questions. Do not repeat a", "question that clarifications or earlier runs already answered or declined. Only ask when you", - "truly cannot proceed; a reasonable assumption about what the user means, stated in", - "\"assumptions\", is better than a question.", + "truly cannot proceed, or when a capability's when_to_use, a fact in", + "capability_availability.deliverables or the workflow instructions say to ask; otherwise a", + "reasonable assumption about what the user means, stated in \"assumptions\", is better than a", + "question.", "", "", "Starting saved workflows. workflow_planning.catalog.workflows lists the user's saved workflows", @@ -937,7 +975,11 @@ "invent one. If the user already selected documents, plan around those documents.", "Use each candidate's server-resolved source_kind, not its display label, to choose compatible", "work. Never send tabular source IDs to document_analyze or document_compare: those steps", - "do not admit native tabular inputs. For mixed narrative/tabular comparisons, prepare each", + "do not admit native tabular inputs. To combine the rows of several tabular sources into one", + "table or file, use tabular_merge with every one of those sources, never compose; it appends", + "rows and cannot match rows on a key, so ask when the user may mean that. When their columns", + "may differ, choose union, aliases or exclusion, or inspect them with tabular_inspect first.", + "For mixed narrative/tabular comparisons, prepare each", "source with compatible offered capabilities and compose their named results. Ask each", "preparation step only for its own sources' contribution, such as values, periods, units and", "identifiers. A preparation step sees only its own sources, so never ask it to compare them", @@ -1093,8 +1135,10 @@ "user's request, without replacing the original \"message\" or user selections. Plan around", "accepted source identities and explanations, including on repeated questions. Do not repeat a", "question that clarifications or earlier runs already answered or declined. Only ask when you", - "truly cannot proceed; a reasonable assumption about what the user means, stated in", - "\"assumptions\", is better than a question.", + "truly cannot proceed, or when a capability's when_to_use, a fact in", + "capability_availability.deliverables or the workflow instructions say to ask; otherwise a", + "reasonable assumption about what the user means, stated in \"assumptions\", is better than a", + "question.", "", "", "Workflows. \"workflow_planning\" is present because this user may be offered one personal", @@ -1132,6 +1176,26 @@ " words such as \"this week\" can stay relative.", "- optionally \"alerts\" {\"mode\":\"every_run\"|\"failures_only\",\"severity\":\"info\"|\"low\"}, \"run_as\"", " \"self\"|\"none\", and \"durable\": true.", + "- A task can merge files with code instead of a model by adding \"merge\": {\"kind\",\"files\",", + " \"output_format\",\"file_name\",\"options\"} and no agent runner. kind \"tabular\" (the default) appends", + " the rows of CSV and Excel files into one \"csv\" or \"xlsx\" file; \"workbook\" puts each CSV or Excel", + " file on its own sheet of one \"xlsx\" workbook; \"pdf\" joins PDFs into one \"pdf\"; \"docx\" appends", + " Word documents into one \"docx\"; \"pptx\" appends the slides of PowerPoint decks into one \"pptx\".", + " When the user wants spreadsheets merged into one Excel file, or names no format, and has not", + " said whether every row goes on one sheet (kind \"tabular\" with \"xlsx\") or each file keeps its", + " own sheet (kind \"workbook\"), ask before proposing: one required single-choice string enum of", + " exactly \"All rows on one sheet\" and \"Each file on its own sheet\". \"csv\" output always means", + " one sheet.", + " files \"inputs\"", + " merges the task's two or more input documents in order; \"changed\" merges the files a file_sync", + " trigger added or changed; \"all\" merges every matching file in the user's personal workspace;", + " \"recent\" merges those added or changed in the last \"recent_window_minutes\". For tabular,", + " \"options\" takes tabular_merge's column, sheet, duplicate and sort settings, with", + " \"column_aliases\" written as [{\"column\",\"aliases\":[...]}]; workbook takes \"sheets\" and \"sheet\";", + " pdf takes \"bookmarks\"; docx takes \"formatting\", \"page_breaks\" and \"source_headings\"; pptx takes", + " \"formatting\" and \"sections\". Propose one when the user wants files merged on a schedule or on", + " every sync, or more files than one chat merge allows; a one-time merge of a few files runs in", + " chat with tabular_merge or document_merge. It needs no task_actions.", "Refer to agents, documents and File Sync sources only by their workflow_planning.catalog handles,", "and never write a record id, model or endpoint into a blueprint. \"task_actions\" lists, for each task", "in order, the action kinds it needs from the capability's input schema, or [] when it needs none.", @@ -1242,7 +1306,11 @@ "invent one. If the user already selected documents, plan around those documents.", "Use each candidate's server-resolved source_kind, not its display label, to choose compatible", "work. Never send tabular source IDs to document_analyze or document_compare: those steps", - "do not admit native tabular inputs. For mixed narrative/tabular comparisons, prepare each", + "do not admit native tabular inputs. To combine the rows of several tabular sources into one", + "table or file, use tabular_merge with every one of those sources, never compose; it appends", + "rows and cannot match rows on a key, so ask when the user may mean that. When their columns", + "may differ, choose union, aliases or exclusion, or inspect them with tabular_inspect first.", + "For mixed narrative/tabular comparisons, prepare each", "source with compatible offered capabilities and compose their named results. Ask each", "preparation step only for its own sources' contribution, such as values, periods, units and", "identifiers. A preparation step sees only its own sources, so never ask it to compare them", @@ -1398,8 +1466,10 @@ "user's request, without replacing the original \"message\" or user selections. Plan around", "accepted source identities and explanations, including on repeated questions. Do not repeat a", "question that clarifications or earlier runs already answered or declined. Only ask when you", - "truly cannot proceed; a reasonable assumption about what the user means, stated in", - "\"assumptions\", is better than a question.", + "truly cannot proceed, or when a capability's when_to_use, a fact in", + "capability_availability.deliverables or the workflow instructions say to ask; otherwise a", + "reasonable assumption about what the user means, stated in \"assumptions\", is better than a", + "question.", "", "", "Workflows. \"workflow_planning\" is present because this user may be offered one personal", @@ -1437,6 +1507,26 @@ " words such as \"this week\" can stay relative.", "- optionally \"alerts\" {\"mode\":\"every_run\"|\"failures_only\",\"severity\":\"info\"|\"low\"}, \"run_as\"", " \"self\"|\"none\", and \"durable\": true.", + "- A task can merge files with code instead of a model by adding \"merge\": {\"kind\",\"files\",", + " \"output_format\",\"file_name\",\"options\"} and no agent runner. kind \"tabular\" (the default) appends", + " the rows of CSV and Excel files into one \"csv\" or \"xlsx\" file; \"workbook\" puts each CSV or Excel", + " file on its own sheet of one \"xlsx\" workbook; \"pdf\" joins PDFs into one \"pdf\"; \"docx\" appends", + " Word documents into one \"docx\"; \"pptx\" appends the slides of PowerPoint decks into one \"pptx\".", + " When the user wants spreadsheets merged into one Excel file, or names no format, and has not", + " said whether every row goes on one sheet (kind \"tabular\" with \"xlsx\") or each file keeps its", + " own sheet (kind \"workbook\"), ask before proposing: one required single-choice string enum of", + " exactly \"All rows on one sheet\" and \"Each file on its own sheet\". \"csv\" output always means", + " one sheet.", + " files \"inputs\"", + " merges the task's two or more input documents in order; \"changed\" merges the files a file_sync", + " trigger added or changed; \"all\" merges every matching file in the user's personal workspace;", + " \"recent\" merges those added or changed in the last \"recent_window_minutes\". For tabular,", + " \"options\" takes tabular_merge's column, sheet, duplicate and sort settings, with", + " \"column_aliases\" written as [{\"column\",\"aliases\":[...]}]; workbook takes \"sheets\" and \"sheet\";", + " pdf takes \"bookmarks\"; docx takes \"formatting\", \"page_breaks\" and \"source_headings\"; pptx takes", + " \"formatting\" and \"sections\". Propose one when the user wants files merged on a schedule or on", + " every sync, or more files than one chat merge allows; a one-time merge of a few files runs in", + " chat with tabular_merge or document_merge. It needs no task_actions.", "Refer to agents, documents and File Sync sources only by their workflow_planning.catalog handles,", "and never write a record id, model or endpoint into a blueprint. \"task_actions\" lists, for each task", "in order, the action kinds it needs from the capability's input schema, or [] when it needs none.", @@ -1941,6 +2031,375 @@ } ] }, + { + "id": "tabular_inspect", + "label": "Inspect spreadsheets", + "role": "gather", + "summary": "Read the sheets, column headers, row counts and a few sample rows of CSV or Excel files, and how their columns line up.", + "when_to_use": "Use before tabular_merge when the files may not share the same columns, or when the user asks what sheets, columns or rows CSV or Excel files have. It reports each file's headers, row counts and sample rows, which files share the same columns, near matches such as \"Customer ID\" and \"customer_id\", a likely title row with the header_row to use, and a suggested schema_policy. A file that cannot be read is reported, not fatal. To line up differently named columns, bind its \"inspection\" output to a compose step that declares a structured-v1 output with profile tabular_column_mapping_v1, and bind that output as tabular_merge's \"mapping\" input. When the user should review the differences first, compose an answer from the inspection and merge in a later turn.", + "inputs": { + "type": "object", + "properties": { + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true, + "description": "The CSV or Excel files to inspect." + }, + "doc_scope": { + "type": "string", + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "One sheet to read in every Excel file. Omit for each workbook's first visible sheet." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "all reads every visible sheet of each workbook; first (default) reads one." + }, + "header_row": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "description": "The row holding the column headers in every file. Omit to use the first nonblank row." + }, + "sample_rows": { + "type": "integer", + "minimum": 0, + "maximum": 10, + "description": "Sample rows to read per sheet; 3 when omitted." + } + }, + "additionalProperties": false + }, + "cost": "low", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "tabular-inspect-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ] + }, + "partial_inputs_supported": false, + "result_outputs": { + "inspection": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "tabular_merge", + "label": "Merge spreadsheets", + "role": "reason", + "summary": "Combine the rows of two or more CSV or Excel files into one table, exactly and without a model, lining up columns by name, alias or a prepared mapping.", + "when_to_use": "Use when the user asks to merge, combine, append, stack or consolidate several CSV or Excel files into one table or file. Every row is kept, in the order the files are listed. It never matches rows on a key column (no joins or lookups) and never edits values; when \"merge\" could mean matching rows on a key, ask. Name the files in document_ids in merge order, or bind inputs.sources to a search step's sources output when the files must be found first. schema_policy: by_name (default) needs the same columns in any order, exact_order also the same order, union keeps every column from every file and leaves a missing one blank, mapped keeps exactly \"columns\". column_aliases lines up other header names, e.g. {\"Customer ID\": [\"cust_id\"]}. When the columns may differ and how they line up is unclear, run tabular_inspect first and bind a compose step's tabular_column_mapping_v1 output as the \"mapping\" input, without columns or aliases. on_incompatible exclude leaves out, and reports, files or sheets that do not fit instead of failing. header_row skips title rows above the headers. sheet picks one sheet; sheets all merges every visible sheet and adds a Source Sheet column. dedupe removes duplicate rows: exact_rows, or key_columns with dedupe_columns, keeping the first or last (dedupe_keep). sort_by orders rows by up to three columns. include_source_column adds each row's file name. To deliver a file, bind the records output to render_file with csv and exact_tabular_records_v1, or xlsx and exact_tabular_workbook_v1. Never compose merged rows. A merge of more files than the chat limit, or one that should repeat, belongs in a workflow.", + "inputs": { + "type": "object", + "properties": { + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 2, + "uniqueItems": true, + "description": "The CSV or Excel files to merge, in the order their rows should appear." + }, + "doc_scope": { + "type": "string", + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "schema_policy": { + "type": "string", + "enum": [ + "by_name", + "exact_order", + "union", + "mapped" + ], + "default": "by_name", + "description": "by_name: same columns in any order. exact_order: same columns in the same order. union: every column from every file. mapped: exactly \"columns\"." + }, + "columns": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 255, + "uniqueItems": true, + "description": "mapped only: the merged columns, in order." + }, + "column_aliases": { + "type": "object", + "maxProperties": 256, + "additionalProperties": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 32, + "uniqueItems": true + }, + "description": "Each column name mapped to the other header names that mean it." + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "One sheet to read in every Excel file. Omit for each workbook's first visible sheet." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "all reads every visible sheet of each workbook; first (default) reads one." + }, + "header_row": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "description": "The row holding the column headers in every file. Omit to use the first nonblank row." + }, + "include_source_column": { + "type": "boolean", + "default": true, + "description": "Add a column holding each row's file name." + }, + "source_column_name": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "default": "Source File" + }, + "on_incompatible": { + "type": "string", + "enum": [ + "fail", + "exclude" + ], + "description": "fail (default) merges nothing when a file does not fit; exclude leaves it out." + }, + "dedupe": { + "type": "string", + "enum": [ + "none", + "exact_rows", + "key_columns" + ], + "description": "Remove duplicate rows; none when omitted." + }, + "dedupe_columns": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 16, + "uniqueItems": true, + "description": "key_columns only: the columns that identify a duplicate." + }, + "dedupe_keep": { + "type": "string", + "enum": [ + "first", + "last" + ], + "description": "Which duplicate to keep; first when omitted." + }, + "sort_by": { + "type": "array", + "maxItems": 3, + "items": { + "type": "object", + "properties": { + "column": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "descending": { + "type": "boolean" + }, + "value_type": { + "type": "string", + "enum": [ + "text", + "number", + "date" + ] + } + }, + "required": [ + "column" + ], + "additionalProperties": false + }, + "description": "Sort keys, most significant first; text ascending when not stated." + } + }, + "additionalProperties": false + }, + "cost": "medium", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "tabular-merge-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ], + "mapping": [ + "structured-v1" + ] + }, + "partial_inputs_supported": false, + "result_outputs": { + "records": "records-v1", + "report": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "document_merge", + "label": "Merge documents", + "role": "reason", + "summary": "Combine two or more PDFs, Word documents or PowerPoint decks into one file of the same kind, or CSV and Excel files into one workbook with a sheet per file, exactly and without a model.", + "when_to_use": "Use when the user asks to combine, join, append, put together or bind several PDFs, Word documents or PowerPoint decks into one file, or to keep several CSV or Excel files as separate sheets of one workbook. Files are copied exactly, in the order listed; nothing is summarized or rewritten. Name the files in document_ids in merge order, or bind inputs.sources to a search step's sources output when the files must be found first. kind is required: pdf for PDFs (bookmarks false leaves out the bookmark per file); docx for Word documents (formatting keep_source or use_first, page_breaks, source_headings); pptx for PowerPoint decks (formatting, sections); workbook for CSV or Excel files, one sheet per file (sheets all copies every visible sheet, or sheet names one). Every file must be the kind being merged. To deliver the file, bind the assembly output to render_file with profile assembled_document_v1 and output_format pdf, docx, pptx, or xlsx for workbook. To append the rows of spreadsheets into one table, use tabular_merge instead. A merge of more files than the chat limit, or one that should repeat, belongs in a workflow.", + "inputs": { + "type": "object", + "properties": { + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 2, + "uniqueItems": true, + "description": "The files to merge, in the order they should appear." + }, + "doc_scope": { + "type": "string", + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "kind": { + "type": "string", + "enum": [ + "pdf", + "docx", + "pptx", + "workbook" + ], + "description": "The kind of file to create; every file must be that kind." + }, + "bookmarks": { + "type": "boolean", + "description": "pdf only: a bookmark named after each file; true when omitted." + }, + "formatting": { + "type": "string", + "enum": [ + "keep_source", + "use_first" + ], + "description": "docx and pptx only: keep_source (default) keeps each file's look; use_first uses the first file's styles or theme for all of them." + }, + "page_breaks": { + "type": "boolean", + "description": "docx only: start each document on a new page; true when omitted." + }, + "source_headings": { + "type": "boolean", + "description": "docx only: put a heading with the file name before each document." + }, + "sections": { + "type": "boolean", + "description": "pptx only: group each deck's slides in a section; true when omitted." + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "workbook only: the one sheet to copy from every file." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "workbook only: all copies every visible sheet; first (default) copies one." + } + }, + "required": [ + "kind" + ], + "additionalProperties": false + }, + "cost": "medium", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "document-merge-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ] + }, + "partial_inputs_supported": false, + "result_outputs": { + "assembly": "structured-v1", + "report": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, { "id": "web_search", "label": "Search the web", @@ -2650,29 +3109,96 @@ } } } - } - }, - "capability_availability": { - "available": [ - "document_search", - "document_analyze", - "document_compare", - "tabular_analyze", - "web_search", - "action_invoke", - "agent_invoke", - "compose", - "workflow_propose", - "workflow_run", - "workflow_results" - ], - "unavailable": { - "url_fetch": "feature_disabled", - "deep_research": "feature_disabled", - "generate_image": "feature_disabled", - "render_file": "export_catalog_unavailable" }, - "web_discovery_enabled": true, + "tabular_column_mapping_v1": { + "type": "object", + "description": "How to line up the columns of several CSV or Excel files before merging them. columns is the merged table's columns in order; use the clearest existing header for each. mappings lists every header whose name differs from the column it fills, and every header to leave out (target_column null). A header that already matches a column, ignoring case and spacing, needs no entry. Map a header only when it holds the same kind of value; when unsure, keep it as its own column or mark confidence low.", + "properties": { + "columns": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 255, + "uniqueItems": true + }, + "mappings": { + "type": "array", + "maxItems": 1024, + "items": { + "type": "object", + "properties": { + "source_column": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "A header exactly as it appears in a file." + }, + "target_column": { + "type": [ + "string", + "null" + ], + "maxLength": 256, + "description": "The column in columns this header fills, or null to leave it out." + }, + "confidence": { + "type": "string", + "enum": [ + "high", + "medium", + "low" + ] + } + }, + "required": [ + "source_column", + "target_column", + "confidence" + ], + "additionalProperties": false + } + }, + "notes": { + "type": "string", + "maxLength": 2000, + "description": "A short explanation of the mapping for the user." + } + }, + "required": [ + "columns", + "mappings" + ], + "additionalProperties": false + } + }, + "capability_availability": { + "available": [ + "document_search", + "document_analyze", + "document_compare", + "tabular_analyze", + "tabular_inspect", + "tabular_merge", + "document_merge", + "web_search", + "action_invoke", + "agent_invoke", + "compose", + "workflow_propose", + "workflow_run", + "workflow_results" + ], + "unavailable": { + "url_fetch": "feature_disabled", + "deep_research": "feature_disabled", + "generate_image": "feature_disabled", + "render_file": "export_catalog_unavailable" + }, + "web_discovery_enabled": true, "visual_outputs": { "charts": true, "diagrams": true, @@ -2697,7 +3223,8 @@ "records-v1" ], "profiles": [ - "tabular_records_v1" + "tabular_records_v1", + "exact_tabular_records_v1" ], "embeds_images": false, "status": "unavailable", @@ -2766,10 +3293,13 @@ }, "xlsx": { "source_kinds": [ - "records-v1" + "records-v1", + "structured-v1" ], "profiles": [ - "tabular_workbook_v1" + "tabular_workbook_v1", + "exact_tabular_workbook_v1", + "assembled_document_v1" ], "embeds_images": false, "status": "unavailable", @@ -2778,10 +3308,12 @@ "docx": { "source_kinds": [ "markdown-v1", + "structured-v1", "text-v1" ], "profiles": [ - "prepared_report_v1" + "prepared_report_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -2790,10 +3322,12 @@ "pdf": { "source_kinds": [ "markdown-v1", + "structured-v1", "text-v1" ], "profiles": [ - "prepared_report_v1" + "prepared_report_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -2804,7 +3338,8 @@ "structured-v1" ], "profiles": [ - "prepared_slide_deck_v1" + "prepared_slide_deck_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -3579,22 +4114,60 @@ ] }, { - "id": "web_search", - "label": "Search the web", + "id": "tabular_inspect", + "label": "Inspect spreadsheets", "role": "gather", - "summary": "Look the question up on the public web.", - "when_to_use": "Use for focused external lookups, current facts, or limited discovery that search results can adequately support. One search can return several sources; that alone does not require deep research. Do not use it to answer questions about the user's own material.", + "summary": "Read the sheets, column headers, row counts and a few sample rows of CSV or Excel files, and how their columns line up.", + "when_to_use": "Use before tabular_merge when the files may not share the same columns, or when the user asks what sheets, columns or rows CSV or Excel files have. It reports each file's headers, row counts and sample rows, which files share the same columns, near matches such as \"Customer ID\" and \"customer_id\", a likely title row with the header_row to use, and a suggested schema_policy. A file that cannot be read is reported, not fatal. To line up differently named columns, bind its \"inspection\" output to a compose step that declares a structured-v1 output with profile tabular_column_mapping_v1, and bind that output as tabular_merge's \"mapping\" input. When the user should review the differences first, compose an answer from the inspection and merge in a later turn.", "inputs": { "type": "object", "properties": { - "query": { + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true, + "description": "The CSV or Excel files to inspect." + }, + "doc_scope": { "type": "string", - "minLength": 1 + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "One sheet to read in every Excel file. Omit for each workbook's first visible sheet." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "all reads every visible sheet of each workbook; first (default) reads one." + }, + "header_row": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "description": "The row holding the column headers in every file. Omit to use the first nonblank row." + }, + "sample_rows": { + "type": "integer", + "minimum": 0, + "maximum": 10, + "description": "Sample rows to read per sheet; 3 when omitted." } }, - "required": [ - "query" - ], "additionalProperties": false }, "cost": "low", @@ -3603,108 +4176,439 @@ ], "max_per_plan": 2, "plan_contract_version": 2, - "result_contract_version": "orchestration-gathered-content-v1", - "result_input_kinds": {}, + "result_contract_version": "tabular-inspect-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ] + }, "partial_inputs_supported": false, "result_outputs": { - "prepared": "structured-v1" + "inspection": "structured-v1" }, "optional_result_outputs": {}, "result_output_kinds": [] }, { - "id": "agent_invoke", - "label": "Ask an agent", - "role": "gather", - "summary": "Hand the task to one of this user's configured agents.", - "when_to_use": "An agent in the list has tools or knowledge built for exactly this task -- reaching a system none of the other capabilities can, or following a procedure somebody configured deliberately. Name only an agent from the list. An agent runs its own tools, so do not also plan the work it would do itself.", + "id": "tabular_merge", + "label": "Merge spreadsheets", + "role": "reason", + "summary": "Combine the rows of two or more CSV or Excel files into one table, exactly and without a model, lining up columns by name, alias or a prepared mapping.", + "when_to_use": "Use when the user asks to merge, combine, append, stack or consolidate several CSV or Excel files into one table or file. Every row is kept, in the order the files are listed. It never matches rows on a key column (no joins or lookups) and never edits values; when \"merge\" could mean matching rows on a key, ask. Name the files in document_ids in merge order, or bind inputs.sources to a search step's sources output when the files must be found first. schema_policy: by_name (default) needs the same columns in any order, exact_order also the same order, union keeps every column from every file and leaves a missing one blank, mapped keeps exactly \"columns\". column_aliases lines up other header names, e.g. {\"Customer ID\": [\"cust_id\"]}. When the columns may differ and how they line up is unclear, run tabular_inspect first and bind a compose step's tabular_column_mapping_v1 output as the \"mapping\" input, without columns or aliases. on_incompatible exclude leaves out, and reports, files or sheets that do not fit instead of failing. header_row skips title rows above the headers. sheet picks one sheet; sheets all merges every visible sheet and adds a Source Sheet column. dedupe removes duplicate rows: exact_rows, or key_columns with dedupe_columns, keeping the first or last (dedupe_keep). sort_by orders rows by up to three columns. include_source_column adds each row's file name. To deliver a file, bind the records output to render_file with csv and exact_tabular_records_v1, or xlsx and exact_tabular_workbook_v1. Never compose merged rows. A merge of more files than the chat limit, or one that should repeat, belongs in a workflow.", "inputs": { "type": "object", "properties": { - "agent_name": { + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 2, + "uniqueItems": true, + "description": "The CSV or Excel files to merge, in the order their rows should appear." + }, + "doc_scope": { + "type": "string", + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "schema_policy": { + "type": "string", + "enum": [ + "by_name", + "exact_order", + "union", + "mapped" + ], + "default": "by_name", + "description": "by_name: same columns in any order. exact_order: same columns in the same order. union: every column from every file. mapped: exactly \"columns\"." + }, + "columns": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 255, + "uniqueItems": true, + "description": "mapped only: the merged columns, in order." + }, + "column_aliases": { + "type": "object", + "maxProperties": 256, + "additionalProperties": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 32, + "uniqueItems": true + }, + "description": "Each column name mapped to the other header names that mean it." + }, + "sheet": { "type": "string", "minLength": 1, - "description": "The agent's name, exactly as it appears in the list." + "maxLength": 31, + "description": "One sheet to read in every Excel file. Omit for each workbook's first visible sheet." }, - "task": { + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "all reads every visible sheet of each workbook; first (default) reads one." + }, + "header_row": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "description": "The row holding the column headers in every file. Omit to use the first nonblank row." + }, + "include_source_column": { + "type": "boolean", + "default": true, + "description": "Add a column holding each row's file name." + }, + "source_column_name": { "type": "string", "minLength": 1, - "description": "What to ask the agent to do, in a sentence or two." + "maxLength": 128, + "default": "Source File" }, - "visuals": { + "on_incompatible": { + "type": "string", + "enum": [ + "fail", + "exclude" + ], + "description": "fail (default) merges nothing when a file does not fit; exclude leaves it out." + }, + "dedupe": { + "type": "string", + "enum": [ + "none", + "exact_rows", + "key_columns" + ], + "description": "Remove duplicate rows; none when omitted." + }, + "dedupe_columns": { "type": "array", "items": { "type": "string", - "enum": [ - "chart", - "diagram", - "image_proposal" - ] + "minLength": 1, + "maxLength": 256 }, + "minItems": 1, + "maxItems": 16, "uniqueItems": true, + "description": "key_columns only: the columns that identify a duplicate." + }, + "dedupe_keep": { + "type": "string", + "enum": [ + "first", + "last" + ], + "description": "Which duplicate to keep; first when omitted." + }, + "sort_by": { + "type": "array", "maxItems": 3, - "description": "Visuals the answer will include, so the agent keeps the values, relationships or visual details they need." + "items": { + "type": "object", + "properties": { + "column": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "descending": { + "type": "boolean" + }, + "value_type": { + "type": "string", + "enum": [ + "text", + "number", + "date" + ] + } + }, + "required": [ + "column" + ], + "additionalProperties": false + }, + "description": "Sort keys, most significant first; text ascending when not stated." } }, - "required": [ - "agent_name", - "task" - ], "additionalProperties": false }, - "cost": "high", + "cost": "medium", "produces": [ "retained_results" ], - "max_per_plan": 1, + "max_per_plan": 2, "plan_contract_version": 2, - "result_contract_version": "orchestration-gathered-content-v1", - "result_input_kinds": {}, + "result_contract_version": "tabular-merge-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ], + "mapping": [ + "structured-v1" + ] + }, "partial_inputs_supported": false, "result_outputs": { - "prepared": "structured-v1" + "records": "records-v1", + "report": "structured-v1" }, "optional_result_outputs": {}, "result_output_kinds": [] }, { - "id": "compose", - "label": "Prepare content", + "id": "document_merge", + "label": "Merge documents", "role": "reason", - "summary": "Compose reusable text, Markdown, records, or structured content.", - "when_to_use": "Explicitly draft an answer or report, or prepare structured data. Bind every retained input by name. Source-free content is supported. This does not create files, infer a file format, retrieve sources, or call tools. Declare each output and select final_response when its text should become the chat answer. Set knowledge_basis: general_knowledge for stable, widely known facts that need no retrieval; sources when every claim must come from the named inputs (private documents, current or local facts); sources_and_general_knowledge when named inputs lead but stable general knowledge may fill gaps. Mark an input optional only when the answer can still be written from general knowledge if that input fails. Name the visuals the Markdown answer should author: chart, diagram (Mermaid), or image_proposal (cards the user approves before generation). Place bound generate_image outputs with [[image:]] tokens in Markdown, or as \"asset:\" image sources in a prepared slide deck.", + "summary": "Combine two or more PDFs, Word documents or PowerPoint decks into one file of the same kind, or CSV and Excel files into one workbook with a sheet per file, exactly and without a model.", + "when_to_use": "Use when the user asks to combine, join, append, put together or bind several PDFs, Word documents or PowerPoint decks into one file, or to keep several CSV or Excel files as separate sheets of one workbook. Files are copied exactly, in the order listed; nothing is summarized or rewritten. Name the files in document_ids in merge order, or bind inputs.sources to a search step's sources output when the files must be found first. kind is required: pdf for PDFs (bookmarks false leaves out the bookmark per file); docx for Word documents (formatting keep_source or use_first, page_breaks, source_headings); pptx for PowerPoint decks (formatting, sections); workbook for CSV or Excel files, one sheet per file (sheets all copies every visible sheet, or sheet names one). Every file must be the kind being merged. To deliver the file, bind the assembly output to render_file with profile assembled_document_v1 and output_format pdf, docx, pptx, or xlsx for workbook. To append the rows of spreadsheets into one table, use tabular_merge instead. A merge of more files than the chat limit, or one that should repeat, belongs in a workflow.", "inputs": { "type": "object", "properties": { - "instruction": { + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 2, + "uniqueItems": true, + "description": "The files to merge, in the order they should appear." + }, + "doc_scope": { "type": "string", - "minLength": 1 + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" }, - "knowledge_basis": { + "kind": { "type": "string", "enum": [ - "general_knowledge", - "sources", - "sources_and_general_knowledge" - ] + "pdf", + "docx", + "pptx", + "workbook" + ], + "description": "The kind of file to create; every file must be that kind." }, - "visuals": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "chart", - "diagram", - "image_proposal" - ] - }, - "uniqueItems": true, - "maxItems": 3 - } - }, - "required": [ - "instruction" - ], + "bookmarks": { + "type": "boolean", + "description": "pdf only: a bookmark named after each file; true when omitted." + }, + "formatting": { + "type": "string", + "enum": [ + "keep_source", + "use_first" + ], + "description": "docx and pptx only: keep_source (default) keeps each file's look; use_first uses the first file's styles or theme for all of them." + }, + "page_breaks": { + "type": "boolean", + "description": "docx only: start each document on a new page; true when omitted." + }, + "source_headings": { + "type": "boolean", + "description": "docx only: put a heading with the file name before each document." + }, + "sections": { + "type": "boolean", + "description": "pptx only: group each deck's slides in a section; true when omitted." + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "workbook only: the one sheet to copy from every file." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "workbook only: all copies every visible sheet; first (default) copies one." + } + }, + "required": [ + "kind" + ], + "additionalProperties": false + }, + "cost": "medium", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "document-merge-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ] + }, + "partial_inputs_supported": false, + "result_outputs": { + "assembly": "structured-v1", + "report": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "web_search", + "label": "Search the web", + "role": "gather", + "summary": "Look the question up on the public web.", + "when_to_use": "Use for focused external lookups, current facts, or limited discovery that search results can adequately support. One search can return several sources; that alone does not require deep research. Do not use it to answer questions about the user's own material.", + "inputs": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "query" + ], + "additionalProperties": false + }, + "cost": "low", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "orchestration-gathered-content-v1", + "result_input_kinds": {}, + "partial_inputs_supported": false, + "result_outputs": { + "prepared": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "agent_invoke", + "label": "Ask an agent", + "role": "gather", + "summary": "Hand the task to one of this user's configured agents.", + "when_to_use": "An agent in the list has tools or knowledge built for exactly this task -- reaching a system none of the other capabilities can, or following a procedure somebody configured deliberately. Name only an agent from the list. An agent runs its own tools, so do not also plan the work it would do itself.", + "inputs": { + "type": "object", + "properties": { + "agent_name": { + "type": "string", + "minLength": 1, + "description": "The agent's name, exactly as it appears in the list." + }, + "task": { + "type": "string", + "minLength": 1, + "description": "What to ask the agent to do, in a sentence or two." + }, + "visuals": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "chart", + "diagram", + "image_proposal" + ] + }, + "uniqueItems": true, + "maxItems": 3, + "description": "Visuals the answer will include, so the agent keeps the values, relationships or visual details they need." + } + }, + "required": [ + "agent_name", + "task" + ], + "additionalProperties": false + }, + "cost": "high", + "produces": [ + "retained_results" + ], + "max_per_plan": 1, + "plan_contract_version": 2, + "result_contract_version": "orchestration-gathered-content-v1", + "result_input_kinds": {}, + "partial_inputs_supported": false, + "result_outputs": { + "prepared": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "compose", + "label": "Prepare content", + "role": "reason", + "summary": "Compose reusable text, Markdown, records, or structured content.", + "when_to_use": "Explicitly draft an answer or report, or prepare structured data. Bind every retained input by name. Source-free content is supported. This does not create files, infer a file format, retrieve sources, or call tools. Declare each output and select final_response when its text should become the chat answer. Set knowledge_basis: general_knowledge for stable, widely known facts that need no retrieval; sources when every claim must come from the named inputs (private documents, current or local facts); sources_and_general_knowledge when named inputs lead but stable general knowledge may fill gaps. Mark an input optional only when the answer can still be written from general knowledge if that input fails. Name the visuals the Markdown answer should author: chart, diagram (Mermaid), or image_proposal (cards the user approves before generation). Place bound generate_image outputs with [[image:]] tokens in Markdown, or as \"asset:\" image sources in a prepared slide deck.", + "inputs": { + "type": "object", + "properties": { + "instruction": { + "type": "string", + "minLength": 1 + }, + "knowledge_basis": { + "type": "string", + "enum": [ + "general_knowledge", + "sources", + "sources_and_general_knowledge" + ] + }, + "visuals": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "chart", + "diagram", + "image_proposal" + ] + }, + "uniqueItems": true, + "maxItems": 3 + } + }, + "required": [ + "instruction" + ], "additionalProperties": false }, "cost": "low", @@ -4087,6 +4991,70 @@ } } } + }, + "tabular_column_mapping_v1": { + "type": "object", + "description": "How to line up the columns of several CSV or Excel files before merging them. columns is the merged table's columns in order; use the clearest existing header for each. mappings lists every header whose name differs from the column it fills, and every header to leave out (target_column null). A header that already matches a column, ignoring case and spacing, needs no entry. Map a header only when it holds the same kind of value; when unsure, keep it as its own column or mark confidence low.", + "properties": { + "columns": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 255, + "uniqueItems": true + }, + "mappings": { + "type": "array", + "maxItems": 1024, + "items": { + "type": "object", + "properties": { + "source_column": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "A header exactly as it appears in a file." + }, + "target_column": { + "type": [ + "string", + "null" + ], + "maxLength": 256, + "description": "The column in columns this header fills, or null to leave it out." + }, + "confidence": { + "type": "string", + "enum": [ + "high", + "medium", + "low" + ] + } + }, + "required": [ + "source_column", + "target_column", + "confidence" + ], + "additionalProperties": false + } + }, + "notes": { + "type": "string", + "maxLength": 2000, + "description": "A short explanation of the mapping for the user." + } + }, + "required": [ + "columns", + "mappings" + ], + "additionalProperties": false } }, "capability_availability": { @@ -4095,6 +5063,9 @@ "document_analyze", "document_compare", "tabular_analyze", + "tabular_inspect", + "tabular_merge", + "document_merge", "web_search", "agent_invoke", "compose" @@ -4134,7 +5105,8 @@ "records-v1" ], "profiles": [ - "tabular_records_v1" + "tabular_records_v1", + "exact_tabular_records_v1" ], "embeds_images": false, "status": "unavailable", @@ -4203,10 +5175,13 @@ }, "xlsx": { "source_kinds": [ - "records-v1" + "records-v1", + "structured-v1" ], "profiles": [ - "tabular_workbook_v1" + "tabular_workbook_v1", + "exact_tabular_workbook_v1", + "assembled_document_v1" ], "embeds_images": false, "status": "unavailable", @@ -4215,10 +5190,12 @@ "docx": { "source_kinds": [ "markdown-v1", + "structured-v1", "text-v1" ], "profiles": [ - "prepared_report_v1" + "prepared_report_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -4227,10 +5204,12 @@ "pdf": { "source_kinds": [ "markdown-v1", + "structured-v1", "text-v1" ], "profiles": [ - "prepared_report_v1" + "prepared_report_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -4241,7 +5220,8 @@ "structured-v1" ], "profiles": [ - "prepared_slide_deck_v1" + "prepared_slide_deck_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -4772,22 +5752,60 @@ ] }, { - "id": "web_search", - "label": "Search the web", + "id": "tabular_inspect", + "label": "Inspect spreadsheets", "role": "gather", - "summary": "Look the question up on the public web.", - "when_to_use": "Use for focused external lookups, current facts, or limited discovery that search results can adequately support. One search can return several sources; that alone does not require deep research. Do not use it to answer questions about the user's own material.", + "summary": "Read the sheets, column headers, row counts and a few sample rows of CSV or Excel files, and how their columns line up.", + "when_to_use": "Use before tabular_merge when the files may not share the same columns, or when the user asks what sheets, columns or rows CSV or Excel files have. It reports each file's headers, row counts and sample rows, which files share the same columns, near matches such as \"Customer ID\" and \"customer_id\", a likely title row with the header_row to use, and a suggested schema_policy. A file that cannot be read is reported, not fatal. To line up differently named columns, bind its \"inspection\" output to a compose step that declares a structured-v1 output with profile tabular_column_mapping_v1, and bind that output as tabular_merge's \"mapping\" input. When the user should review the differences first, compose an answer from the inspection and merge in a later turn.", "inputs": { "type": "object", "properties": { - "query": { + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true, + "description": "The CSV or Excel files to inspect." + }, + "doc_scope": { "type": "string", - "minLength": 1 + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "One sheet to read in every Excel file. Omit for each workbook's first visible sheet." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "all reads every visible sheet of each workbook; first (default) reads one." + }, + "header_row": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "description": "The row holding the column headers in every file. Omit to use the first nonblank row." + }, + "sample_rows": { + "type": "integer", + "minimum": 0, + "maximum": 10, + "description": "Sample rows to read per sheet; 3 when omitted." } }, - "required": [ - "query" - ], "additionalProperties": false }, "cost": "low", @@ -4796,31 +5814,362 @@ ], "max_per_plan": 2, "plan_contract_version": 2, - "result_contract_version": "orchestration-gathered-content-v1", - "result_input_kinds": {}, + "result_contract_version": "tabular-inspect-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ] + }, "partial_inputs_supported": false, "result_outputs": { - "prepared": "structured-v1" + "inspection": "structured-v1" }, "optional_result_outputs": {}, "result_output_kinds": [] }, { - "id": "action_invoke", - "label": "Use an action", - "role": "gather", - "summary": "Gather knowledge using one of this user's accessible actions.", - "when_to_use": "An action in the list reaches the information needed for this task. Prefer using it directly when no agent-specific instructions or knowledge are needed. The step can use the selected action's functions within execution limits. Do not duplicate work delegated to an agent or use this to plan output tasks.", + "id": "tabular_merge", + "label": "Merge spreadsheets", + "role": "reason", + "summary": "Combine the rows of two or more CSV or Excel files into one table, exactly and without a model, lining up columns by name, alias or a prepared mapping.", + "when_to_use": "Use when the user asks to merge, combine, append, stack or consolidate several CSV or Excel files into one table or file. Every row is kept, in the order the files are listed. It never matches rows on a key column (no joins or lookups) and never edits values; when \"merge\" could mean matching rows on a key, ask. Name the files in document_ids in merge order, or bind inputs.sources to a search step's sources output when the files must be found first. schema_policy: by_name (default) needs the same columns in any order, exact_order also the same order, union keeps every column from every file and leaves a missing one blank, mapped keeps exactly \"columns\". column_aliases lines up other header names, e.g. {\"Customer ID\": [\"cust_id\"]}. When the columns may differ and how they line up is unclear, run tabular_inspect first and bind a compose step's tabular_column_mapping_v1 output as the \"mapping\" input, without columns or aliases. on_incompatible exclude leaves out, and reports, files or sheets that do not fit instead of failing. header_row skips title rows above the headers. sheet picks one sheet; sheets all merges every visible sheet and adds a Source Sheet column. dedupe removes duplicate rows: exact_rows, or key_columns with dedupe_columns, keeping the first or last (dedupe_keep). sort_by orders rows by up to three columns. include_source_column adds each row's file name. To deliver a file, bind the records output to render_file with csv and exact_tabular_records_v1, or xlsx and exact_tabular_workbook_v1. Never compose merged rows. A merge of more files than the chat limit, or one that should repeat, belongs in a workflow.", "inputs": { "type": "object", "properties": { - "action_ref": { - "type": "string", - "minLength": 1, - "description": "The exact scoped reference from the actions catalog." - }, - "task": { - "type": "string", + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 2, + "uniqueItems": true, + "description": "The CSV or Excel files to merge, in the order their rows should appear." + }, + "doc_scope": { + "type": "string", + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "schema_policy": { + "type": "string", + "enum": [ + "by_name", + "exact_order", + "union", + "mapped" + ], + "default": "by_name", + "description": "by_name: same columns in any order. exact_order: same columns in the same order. union: every column from every file. mapped: exactly \"columns\"." + }, + "columns": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 255, + "uniqueItems": true, + "description": "mapped only: the merged columns, in order." + }, + "column_aliases": { + "type": "object", + "maxProperties": 256, + "additionalProperties": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 32, + "uniqueItems": true + }, + "description": "Each column name mapped to the other header names that mean it." + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "One sheet to read in every Excel file. Omit for each workbook's first visible sheet." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "all reads every visible sheet of each workbook; first (default) reads one." + }, + "header_row": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "description": "The row holding the column headers in every file. Omit to use the first nonblank row." + }, + "include_source_column": { + "type": "boolean", + "default": true, + "description": "Add a column holding each row's file name." + }, + "source_column_name": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "default": "Source File" + }, + "on_incompatible": { + "type": "string", + "enum": [ + "fail", + "exclude" + ], + "description": "fail (default) merges nothing when a file does not fit; exclude leaves it out." + }, + "dedupe": { + "type": "string", + "enum": [ + "none", + "exact_rows", + "key_columns" + ], + "description": "Remove duplicate rows; none when omitted." + }, + "dedupe_columns": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 16, + "uniqueItems": true, + "description": "key_columns only: the columns that identify a duplicate." + }, + "dedupe_keep": { + "type": "string", + "enum": [ + "first", + "last" + ], + "description": "Which duplicate to keep; first when omitted." + }, + "sort_by": { + "type": "array", + "maxItems": 3, + "items": { + "type": "object", + "properties": { + "column": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "descending": { + "type": "boolean" + }, + "value_type": { + "type": "string", + "enum": [ + "text", + "number", + "date" + ] + } + }, + "required": [ + "column" + ], + "additionalProperties": false + }, + "description": "Sort keys, most significant first; text ascending when not stated." + } + }, + "additionalProperties": false + }, + "cost": "medium", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "tabular-merge-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ], + "mapping": [ + "structured-v1" + ] + }, + "partial_inputs_supported": false, + "result_outputs": { + "records": "records-v1", + "report": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "document_merge", + "label": "Merge documents", + "role": "reason", + "summary": "Combine two or more PDFs, Word documents or PowerPoint decks into one file of the same kind, or CSV and Excel files into one workbook with a sheet per file, exactly and without a model.", + "when_to_use": "Use when the user asks to combine, join, append, put together or bind several PDFs, Word documents or PowerPoint decks into one file, or to keep several CSV or Excel files as separate sheets of one workbook. Files are copied exactly, in the order listed; nothing is summarized or rewritten. Name the files in document_ids in merge order, or bind inputs.sources to a search step's sources output when the files must be found first. kind is required: pdf for PDFs (bookmarks false leaves out the bookmark per file); docx for Word documents (formatting keep_source or use_first, page_breaks, source_headings); pptx for PowerPoint decks (formatting, sections); workbook for CSV or Excel files, one sheet per file (sheets all copies every visible sheet, or sheet names one). Every file must be the kind being merged. To deliver the file, bind the assembly output to render_file with profile assembled_document_v1 and output_format pdf, docx, pptx, or xlsx for workbook. To append the rows of spreadsheets into one table, use tabular_merge instead. A merge of more files than the chat limit, or one that should repeat, belongs in a workflow.", + "inputs": { + "type": "object", + "properties": { + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 2, + "uniqueItems": true, + "description": "The files to merge, in the order they should appear." + }, + "doc_scope": { + "type": "string", + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "kind": { + "type": "string", + "enum": [ + "pdf", + "docx", + "pptx", + "workbook" + ], + "description": "The kind of file to create; every file must be that kind." + }, + "bookmarks": { + "type": "boolean", + "description": "pdf only: a bookmark named after each file; true when omitted." + }, + "formatting": { + "type": "string", + "enum": [ + "keep_source", + "use_first" + ], + "description": "docx and pptx only: keep_source (default) keeps each file's look; use_first uses the first file's styles or theme for all of them." + }, + "page_breaks": { + "type": "boolean", + "description": "docx only: start each document on a new page; true when omitted." + }, + "source_headings": { + "type": "boolean", + "description": "docx only: put a heading with the file name before each document." + }, + "sections": { + "type": "boolean", + "description": "pptx only: group each deck's slides in a section; true when omitted." + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "workbook only: the one sheet to copy from every file." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "workbook only: all copies every visible sheet; first (default) copies one." + } + }, + "required": [ + "kind" + ], + "additionalProperties": false + }, + "cost": "medium", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "document-merge-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ] + }, + "partial_inputs_supported": false, + "result_outputs": { + "assembly": "structured-v1", + "report": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "web_search", + "label": "Search the web", + "role": "gather", + "summary": "Look the question up on the public web.", + "when_to_use": "Use for focused external lookups, current facts, or limited discovery that search results can adequately support. One search can return several sources; that alone does not require deep research. Do not use it to answer questions about the user's own material.", + "inputs": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "query" + ], + "additionalProperties": false + }, + "cost": "low", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "orchestration-gathered-content-v1", + "result_input_kinds": {}, + "partial_inputs_supported": false, + "result_outputs": { + "prepared": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "action_invoke", + "label": "Use an action", + "role": "gather", + "summary": "Gather knowledge using one of this user's accessible actions.", + "when_to_use": "An action in the list reaches the information needed for this task. Prefer using it directly when no agent-specific instructions or knowledge are needed. The step can use the selected action's functions within execution limits. Do not duplicate work delegated to an agent or use this to plan output tasks.", + "inputs": { + "type": "object", + "properties": { + "action_ref": { + "type": "string", + "minLength": 1, + "description": "The exact scoped reference from the actions catalog." + }, + "task": { + "type": "string", "minLength": 1, "description": "The knowledge to gather with this action." }, @@ -5333,6 +6682,70 @@ } } } + }, + "tabular_column_mapping_v1": { + "type": "object", + "description": "How to line up the columns of several CSV or Excel files before merging them. columns is the merged table's columns in order; use the clearest existing header for each. mappings lists every header whose name differs from the column it fills, and every header to leave out (target_column null). A header that already matches a column, ignoring case and spacing, needs no entry. Map a header only when it holds the same kind of value; when unsure, keep it as its own column or mark confidence low.", + "properties": { + "columns": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 255, + "uniqueItems": true + }, + "mappings": { + "type": "array", + "maxItems": 1024, + "items": { + "type": "object", + "properties": { + "source_column": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "A header exactly as it appears in a file." + }, + "target_column": { + "type": [ + "string", + "null" + ], + "maxLength": 256, + "description": "The column in columns this header fills, or null to leave it out." + }, + "confidence": { + "type": "string", + "enum": [ + "high", + "medium", + "low" + ] + } + }, + "required": [ + "source_column", + "target_column", + "confidence" + ], + "additionalProperties": false + } + }, + "notes": { + "type": "string", + "maxLength": 2000, + "description": "A short explanation of the mapping for the user." + } + }, + "required": [ + "columns", + "mappings" + ], + "additionalProperties": false } }, "capability_availability": { @@ -5341,6 +6754,9 @@ "document_analyze", "document_compare", "tabular_analyze", + "tabular_inspect", + "tabular_merge", + "document_merge", "web_search", "action_invoke", "agent_invoke", @@ -5380,7 +6796,8 @@ "records-v1" ], "profiles": [ - "tabular_records_v1" + "tabular_records_v1", + "exact_tabular_records_v1" ], "embeds_images": false, "status": "unavailable", @@ -5449,10 +6866,13 @@ }, "xlsx": { "source_kinds": [ - "records-v1" + "records-v1", + "structured-v1" ], "profiles": [ - "tabular_workbook_v1" + "tabular_workbook_v1", + "exact_tabular_workbook_v1", + "assembled_document_v1" ], "embeds_images": false, "status": "unavailable", @@ -5461,10 +6881,12 @@ "docx": { "source_kinds": [ "markdown-v1", + "structured-v1", "text-v1" ], "profiles": [ - "prepared_report_v1" + "prepared_report_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -5473,10 +6895,12 @@ "pdf": { "source_kinds": [ "markdown-v1", + "structured-v1", "text-v1" ], "profiles": [ - "prepared_report_v1" + "prepared_report_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -5487,7 +6911,8 @@ "structured-v1" ], "profiles": [ - "prepared_slide_deck_v1" + "prepared_slide_deck_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -6023,6 +7448,375 @@ } ] }, + { + "id": "tabular_inspect", + "label": "Inspect spreadsheets", + "role": "gather", + "summary": "Read the sheets, column headers, row counts and a few sample rows of CSV or Excel files, and how their columns line up.", + "when_to_use": "Use before tabular_merge when the files may not share the same columns, or when the user asks what sheets, columns or rows CSV or Excel files have. It reports each file's headers, row counts and sample rows, which files share the same columns, near matches such as \"Customer ID\" and \"customer_id\", a likely title row with the header_row to use, and a suggested schema_policy. A file that cannot be read is reported, not fatal. To line up differently named columns, bind its \"inspection\" output to a compose step that declares a structured-v1 output with profile tabular_column_mapping_v1, and bind that output as tabular_merge's \"mapping\" input. When the user should review the differences first, compose an answer from the inspection and merge in a later turn.", + "inputs": { + "type": "object", + "properties": { + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true, + "description": "The CSV or Excel files to inspect." + }, + "doc_scope": { + "type": "string", + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "One sheet to read in every Excel file. Omit for each workbook's first visible sheet." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "all reads every visible sheet of each workbook; first (default) reads one." + }, + "header_row": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "description": "The row holding the column headers in every file. Omit to use the first nonblank row." + }, + "sample_rows": { + "type": "integer", + "minimum": 0, + "maximum": 10, + "description": "Sample rows to read per sheet; 3 when omitted." + } + }, + "additionalProperties": false + }, + "cost": "low", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "tabular-inspect-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ] + }, + "partial_inputs_supported": false, + "result_outputs": { + "inspection": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "tabular_merge", + "label": "Merge spreadsheets", + "role": "reason", + "summary": "Combine the rows of two or more CSV or Excel files into one table, exactly and without a model, lining up columns by name, alias or a prepared mapping.", + "when_to_use": "Use when the user asks to merge, combine, append, stack or consolidate several CSV or Excel files into one table or file. Every row is kept, in the order the files are listed. It never matches rows on a key column (no joins or lookups) and never edits values; when \"merge\" could mean matching rows on a key, ask. Name the files in document_ids in merge order, or bind inputs.sources to a search step's sources output when the files must be found first. schema_policy: by_name (default) needs the same columns in any order, exact_order also the same order, union keeps every column from every file and leaves a missing one blank, mapped keeps exactly \"columns\". column_aliases lines up other header names, e.g. {\"Customer ID\": [\"cust_id\"]}. When the columns may differ and how they line up is unclear, run tabular_inspect first and bind a compose step's tabular_column_mapping_v1 output as the \"mapping\" input, without columns or aliases. on_incompatible exclude leaves out, and reports, files or sheets that do not fit instead of failing. header_row skips title rows above the headers. sheet picks one sheet; sheets all merges every visible sheet and adds a Source Sheet column. dedupe removes duplicate rows: exact_rows, or key_columns with dedupe_columns, keeping the first or last (dedupe_keep). sort_by orders rows by up to three columns. include_source_column adds each row's file name. To deliver a file, bind the records output to render_file with csv and exact_tabular_records_v1, or xlsx and exact_tabular_workbook_v1. Never compose merged rows. A merge of more files than the chat limit, or one that should repeat, belongs in a workflow.", + "inputs": { + "type": "object", + "properties": { + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 2, + "uniqueItems": true, + "description": "The CSV or Excel files to merge, in the order their rows should appear." + }, + "doc_scope": { + "type": "string", + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "schema_policy": { + "type": "string", + "enum": [ + "by_name", + "exact_order", + "union", + "mapped" + ], + "default": "by_name", + "description": "by_name: same columns in any order. exact_order: same columns in the same order. union: every column from every file. mapped: exactly \"columns\"." + }, + "columns": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 255, + "uniqueItems": true, + "description": "mapped only: the merged columns, in order." + }, + "column_aliases": { + "type": "object", + "maxProperties": 256, + "additionalProperties": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 32, + "uniqueItems": true + }, + "description": "Each column name mapped to the other header names that mean it." + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "One sheet to read in every Excel file. Omit for each workbook's first visible sheet." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "all reads every visible sheet of each workbook; first (default) reads one." + }, + "header_row": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "description": "The row holding the column headers in every file. Omit to use the first nonblank row." + }, + "include_source_column": { + "type": "boolean", + "default": true, + "description": "Add a column holding each row's file name." + }, + "source_column_name": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "default": "Source File" + }, + "on_incompatible": { + "type": "string", + "enum": [ + "fail", + "exclude" + ], + "description": "fail (default) merges nothing when a file does not fit; exclude leaves it out." + }, + "dedupe": { + "type": "string", + "enum": [ + "none", + "exact_rows", + "key_columns" + ], + "description": "Remove duplicate rows; none when omitted." + }, + "dedupe_columns": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 16, + "uniqueItems": true, + "description": "key_columns only: the columns that identify a duplicate." + }, + "dedupe_keep": { + "type": "string", + "enum": [ + "first", + "last" + ], + "description": "Which duplicate to keep; first when omitted." + }, + "sort_by": { + "type": "array", + "maxItems": 3, + "items": { + "type": "object", + "properties": { + "column": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "descending": { + "type": "boolean" + }, + "value_type": { + "type": "string", + "enum": [ + "text", + "number", + "date" + ] + } + }, + "required": [ + "column" + ], + "additionalProperties": false + }, + "description": "Sort keys, most significant first; text ascending when not stated." + } + }, + "additionalProperties": false + }, + "cost": "medium", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "tabular-merge-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ], + "mapping": [ + "structured-v1" + ] + }, + "partial_inputs_supported": false, + "result_outputs": { + "records": "records-v1", + "report": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "document_merge", + "label": "Merge documents", + "role": "reason", + "summary": "Combine two or more PDFs, Word documents or PowerPoint decks into one file of the same kind, or CSV and Excel files into one workbook with a sheet per file, exactly and without a model.", + "when_to_use": "Use when the user asks to combine, join, append, put together or bind several PDFs, Word documents or PowerPoint decks into one file, or to keep several CSV or Excel files as separate sheets of one workbook. Files are copied exactly, in the order listed; nothing is summarized or rewritten. Name the files in document_ids in merge order, or bind inputs.sources to a search step's sources output when the files must be found first. kind is required: pdf for PDFs (bookmarks false leaves out the bookmark per file); docx for Word documents (formatting keep_source or use_first, page_breaks, source_headings); pptx for PowerPoint decks (formatting, sections); workbook for CSV or Excel files, one sheet per file (sheets all copies every visible sheet, or sheet names one). Every file must be the kind being merged. To deliver the file, bind the assembly output to render_file with profile assembled_document_v1 and output_format pdf, docx, pptx, or xlsx for workbook. To append the rows of spreadsheets into one table, use tabular_merge instead. A merge of more files than the chat limit, or one that should repeat, belongs in a workflow.", + "inputs": { + "type": "object", + "properties": { + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 2, + "uniqueItems": true, + "description": "The files to merge, in the order they should appear." + }, + "doc_scope": { + "type": "string", + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "kind": { + "type": "string", + "enum": [ + "pdf", + "docx", + "pptx", + "workbook" + ], + "description": "The kind of file to create; every file must be that kind." + }, + "bookmarks": { + "type": "boolean", + "description": "pdf only: a bookmark named after each file; true when omitted." + }, + "formatting": { + "type": "string", + "enum": [ + "keep_source", + "use_first" + ], + "description": "docx and pptx only: keep_source (default) keeps each file's look; use_first uses the first file's styles or theme for all of them." + }, + "page_breaks": { + "type": "boolean", + "description": "docx only: start each document on a new page; true when omitted." + }, + "source_headings": { + "type": "boolean", + "description": "docx only: put a heading with the file name before each document." + }, + "sections": { + "type": "boolean", + "description": "pptx only: group each deck's slides in a section; true when omitted." + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "workbook only: the one sheet to copy from every file." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "workbook only: all copies every visible sheet; first (default) copies one." + } + }, + "required": [ + "kind" + ], + "additionalProperties": false + }, + "cost": "medium", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "document-merge-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ] + }, + "partial_inputs_supported": false, + "result_outputs": { + "assembly": "structured-v1", + "report": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, { "id": "web_search", "label": "Search the web", @@ -6677,26 +8471,93 @@ } } } - } - }, - "capability_availability": { - "available": [ - "document_search", - "document_analyze", - "document_compare", - "tabular_analyze", - "web_search", - "action_invoke", - "agent_invoke", - "compose", - "workflow_run", - "workflow_results" - ], - "unavailable": { - "url_fetch": "feature_disabled", - "deep_research": "feature_disabled", - "generate_image": "feature_disabled", - "render_file": "export_catalog_unavailable" + }, + "tabular_column_mapping_v1": { + "type": "object", + "description": "How to line up the columns of several CSV or Excel files before merging them. columns is the merged table's columns in order; use the clearest existing header for each. mappings lists every header whose name differs from the column it fills, and every header to leave out (target_column null). A header that already matches a column, ignoring case and spacing, needs no entry. Map a header only when it holds the same kind of value; when unsure, keep it as its own column or mark confidence low.", + "properties": { + "columns": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 255, + "uniqueItems": true + }, + "mappings": { + "type": "array", + "maxItems": 1024, + "items": { + "type": "object", + "properties": { + "source_column": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "A header exactly as it appears in a file." + }, + "target_column": { + "type": [ + "string", + "null" + ], + "maxLength": 256, + "description": "The column in columns this header fills, or null to leave it out." + }, + "confidence": { + "type": "string", + "enum": [ + "high", + "medium", + "low" + ] + } + }, + "required": [ + "source_column", + "target_column", + "confidence" + ], + "additionalProperties": false + } + }, + "notes": { + "type": "string", + "maxLength": 2000, + "description": "A short explanation of the mapping for the user." + } + }, + "required": [ + "columns", + "mappings" + ], + "additionalProperties": false + } + }, + "capability_availability": { + "available": [ + "document_search", + "document_analyze", + "document_compare", + "tabular_analyze", + "tabular_inspect", + "tabular_merge", + "document_merge", + "web_search", + "action_invoke", + "agent_invoke", + "compose", + "workflow_run", + "workflow_results" + ], + "unavailable": { + "url_fetch": "feature_disabled", + "deep_research": "feature_disabled", + "generate_image": "feature_disabled", + "render_file": "export_catalog_unavailable" }, "web_discovery_enabled": true, "visual_outputs": { @@ -6723,7 +8584,8 @@ "records-v1" ], "profiles": [ - "tabular_records_v1" + "tabular_records_v1", + "exact_tabular_records_v1" ], "embeds_images": false, "status": "unavailable", @@ -6792,10 +8654,13 @@ }, "xlsx": { "source_kinds": [ - "records-v1" + "records-v1", + "structured-v1" ], "profiles": [ - "tabular_workbook_v1" + "tabular_workbook_v1", + "exact_tabular_workbook_v1", + "assembled_document_v1" ], "embeds_images": false, "status": "unavailable", @@ -6804,10 +8669,12 @@ "docx": { "source_kinds": [ "markdown-v1", + "structured-v1", "text-v1" ], "profiles": [ - "prepared_report_v1" + "prepared_report_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -6816,10 +8683,12 @@ "pdf": { "source_kinds": [ "markdown-v1", + "structured-v1", "text-v1" ], "profiles": [ - "prepared_report_v1" + "prepared_report_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -6830,7 +8699,8 @@ "structured-v1" ], "profiles": [ - "prepared_slide_deck_v1" + "prepared_slide_deck_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -7532,22 +9402,60 @@ ] }, { - "id": "web_search", - "label": "Search the web", + "id": "tabular_inspect", + "label": "Inspect spreadsheets", "role": "gather", - "summary": "Look the question up on the public web.", - "when_to_use": "Use for focused external lookups, current facts, or limited discovery that search results can adequately support. One search can return several sources; that alone does not require deep research. Do not use it to answer questions about the user's own material.", + "summary": "Read the sheets, column headers, row counts and a few sample rows of CSV or Excel files, and how their columns line up.", + "when_to_use": "Use before tabular_merge when the files may not share the same columns, or when the user asks what sheets, columns or rows CSV or Excel files have. It reports each file's headers, row counts and sample rows, which files share the same columns, near matches such as \"Customer ID\" and \"customer_id\", a likely title row with the header_row to use, and a suggested schema_policy. A file that cannot be read is reported, not fatal. To line up differently named columns, bind its \"inspection\" output to a compose step that declares a structured-v1 output with profile tabular_column_mapping_v1, and bind that output as tabular_merge's \"mapping\" input. When the user should review the differences first, compose an answer from the inspection and merge in a later turn.", "inputs": { "type": "object", "properties": { - "query": { + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true, + "description": "The CSV or Excel files to inspect." + }, + "doc_scope": { "type": "string", - "minLength": 1 + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "One sheet to read in every Excel file. Omit for each workbook's first visible sheet." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "all reads every visible sheet of each workbook; first (default) reads one." + }, + "header_row": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "description": "The row holding the column headers in every file. Omit to use the first nonblank row." + }, + "sample_rows": { + "type": "integer", + "minimum": 0, + "maximum": 10, + "description": "Sample rows to read per sheet; 3 when omitted." } }, - "required": [ - "query" - ], "additionalProperties": false }, "cost": "low", @@ -7556,139 +9464,470 @@ ], "max_per_plan": 2, "plan_contract_version": 2, - "result_contract_version": "orchestration-gathered-content-v1", - "result_input_kinds": {}, + "result_contract_version": "tabular-inspect-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ] + }, "partial_inputs_supported": false, "result_outputs": { - "prepared": "structured-v1" + "inspection": "structured-v1" }, "optional_result_outputs": {}, "result_output_kinds": [] }, { - "id": "action_invoke", - "label": "Use an action", - "role": "gather", - "summary": "Gather knowledge using one of this user's accessible actions.", - "when_to_use": "An action in the list reaches the information needed for this task. Prefer using it directly when no agent-specific instructions or knowledge are needed. The step can use the selected action's functions within execution limits. Do not duplicate work delegated to an agent or use this to plan output tasks.", + "id": "tabular_merge", + "label": "Merge spreadsheets", + "role": "reason", + "summary": "Combine the rows of two or more CSV or Excel files into one table, exactly and without a model, lining up columns by name, alias or a prepared mapping.", + "when_to_use": "Use when the user asks to merge, combine, append, stack or consolidate several CSV or Excel files into one table or file. Every row is kept, in the order the files are listed. It never matches rows on a key column (no joins or lookups) and never edits values; when \"merge\" could mean matching rows on a key, ask. Name the files in document_ids in merge order, or bind inputs.sources to a search step's sources output when the files must be found first. schema_policy: by_name (default) needs the same columns in any order, exact_order also the same order, union keeps every column from every file and leaves a missing one blank, mapped keeps exactly \"columns\". column_aliases lines up other header names, e.g. {\"Customer ID\": [\"cust_id\"]}. When the columns may differ and how they line up is unclear, run tabular_inspect first and bind a compose step's tabular_column_mapping_v1 output as the \"mapping\" input, without columns or aliases. on_incompatible exclude leaves out, and reports, files or sheets that do not fit instead of failing. header_row skips title rows above the headers. sheet picks one sheet; sheets all merges every visible sheet and adds a Source Sheet column. dedupe removes duplicate rows: exact_rows, or key_columns with dedupe_columns, keeping the first or last (dedupe_keep). sort_by orders rows by up to three columns. include_source_column adds each row's file name. To deliver a file, bind the records output to render_file with csv and exact_tabular_records_v1, or xlsx and exact_tabular_workbook_v1. Never compose merged rows. A merge of more files than the chat limit, or one that should repeat, belongs in a workflow.", "inputs": { "type": "object", "properties": { - "action_ref": { + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 2, + "uniqueItems": true, + "description": "The CSV or Excel files to merge, in the order their rows should appear." + }, + "doc_scope": { "type": "string", - "minLength": 1, - "description": "The exact scoped reference from the actions catalog." + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" }, - "task": { + "schema_policy": { "type": "string", - "minLength": 1, - "description": "The knowledge to gather with this action." + "enum": [ + "by_name", + "exact_order", + "union", + "mapped" + ], + "default": "by_name", + "description": "by_name: same columns in any order. exact_order: same columns in the same order. union: every column from every file. mapped: exactly \"columns\"." }, - "visuals": { + "columns": { "type": "array", "items": { "type": "string", - "enum": [ - "chart" - ] + "minLength": 1, + "maxLength": 256 }, + "minItems": 1, + "maxItems": 255, "uniqueItems": true, - "maxItems": 1, - "description": "Include \"chart\" to have this step chart the exact rows its functions return. The chart is drawn from the retrieved data, not from the prose findings." - } - }, - "required": [ - "action_ref", - "task" - ], - "additionalProperties": false - }, - "cost": "medium", - "produces": [ - "retained_results" - ], - "max_per_plan": null, - "plan_contract_version": 2, - "result_contract_version": "orchestration-gathered-content-v1", - "result_input_kinds": {}, - "partial_inputs_supported": false, - "result_outputs": { - "prepared": "structured-v1" - }, - "optional_result_outputs": {}, - "result_output_kinds": [] - }, - { - "id": "agent_invoke", - "label": "Ask an agent", - "role": "gather", - "summary": "Hand the task to one of this user's configured agents.", - "when_to_use": "An agent in the list has tools or knowledge built for exactly this task -- reaching a system none of the other capabilities can, or following a procedure somebody configured deliberately. Name only an agent from the list. An agent runs its own tools, so do not also plan the work it would do itself.", - "inputs": { - "type": "object", - "properties": { - "agent_name": { + "description": "mapped only: the merged columns, in order." + }, + "column_aliases": { + "type": "object", + "maxProperties": 256, + "additionalProperties": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 32, + "uniqueItems": true + }, + "description": "Each column name mapped to the other header names that mean it." + }, + "sheet": { "type": "string", "minLength": 1, - "description": "The agent's name, exactly as it appears in the list." + "maxLength": 31, + "description": "One sheet to read in every Excel file. Omit for each workbook's first visible sheet." }, - "task": { + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "all reads every visible sheet of each workbook; first (default) reads one." + }, + "header_row": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "description": "The row holding the column headers in every file. Omit to use the first nonblank row." + }, + "include_source_column": { + "type": "boolean", + "default": true, + "description": "Add a column holding each row's file name." + }, + "source_column_name": { "type": "string", "minLength": 1, - "description": "What to ask the agent to do, in a sentence or two." + "maxLength": 128, + "default": "Source File" }, - "visuals": { + "on_incompatible": { + "type": "string", + "enum": [ + "fail", + "exclude" + ], + "description": "fail (default) merges nothing when a file does not fit; exclude leaves it out." + }, + "dedupe": { + "type": "string", + "enum": [ + "none", + "exact_rows", + "key_columns" + ], + "description": "Remove duplicate rows; none when omitted." + }, + "dedupe_columns": { "type": "array", "items": { "type": "string", - "enum": [ - "chart", - "diagram", - "image_proposal" - ] + "minLength": 1, + "maxLength": 256 }, + "minItems": 1, + "maxItems": 16, "uniqueItems": true, + "description": "key_columns only: the columns that identify a duplicate." + }, + "dedupe_keep": { + "type": "string", + "enum": [ + "first", + "last" + ], + "description": "Which duplicate to keep; first when omitted." + }, + "sort_by": { + "type": "array", "maxItems": 3, - "description": "Visuals the answer will include, so the agent keeps the values, relationships or visual details they need." + "items": { + "type": "object", + "properties": { + "column": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "descending": { + "type": "boolean" + }, + "value_type": { + "type": "string", + "enum": [ + "text", + "number", + "date" + ] + } + }, + "required": [ + "column" + ], + "additionalProperties": false + }, + "description": "Sort keys, most significant first; text ascending when not stated." } }, - "required": [ - "agent_name", - "task" - ], "additionalProperties": false }, - "cost": "high", + "cost": "medium", "produces": [ "retained_results" ], - "max_per_plan": 1, + "max_per_plan": 2, "plan_contract_version": 2, - "result_contract_version": "orchestration-gathered-content-v1", - "result_input_kinds": {}, + "result_contract_version": "tabular-merge-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ], + "mapping": [ + "structured-v1" + ] + }, "partial_inputs_supported": false, "result_outputs": { - "prepared": "structured-v1" + "records": "records-v1", + "report": "structured-v1" }, "optional_result_outputs": {}, "result_output_kinds": [] }, { - "id": "compose", - "label": "Prepare content", + "id": "document_merge", + "label": "Merge documents", "role": "reason", - "summary": "Compose reusable text, Markdown, records, or structured content.", - "when_to_use": "Explicitly draft an answer or report, or prepare structured data. Bind every retained input by name. Source-free content is supported. This does not create files, infer a file format, retrieve sources, or call tools. Declare each output and select final_response when its text should become the chat answer. Set knowledge_basis: general_knowledge for stable, widely known facts that need no retrieval; sources when every claim must come from the named inputs (private documents, current or local facts); sources_and_general_knowledge when named inputs lead but stable general knowledge may fill gaps. Mark an input optional only when the answer can still be written from general knowledge if that input fails. Name the visuals the Markdown answer should author: chart, diagram (Mermaid), or image_proposal (cards the user approves before generation). Place bound generate_image outputs with [[image:]] tokens in Markdown, or as \"asset:\" image sources in a prepared slide deck.", + "summary": "Combine two or more PDFs, Word documents or PowerPoint decks into one file of the same kind, or CSV and Excel files into one workbook with a sheet per file, exactly and without a model.", + "when_to_use": "Use when the user asks to combine, join, append, put together or bind several PDFs, Word documents or PowerPoint decks into one file, or to keep several CSV or Excel files as separate sheets of one workbook. Files are copied exactly, in the order listed; nothing is summarized or rewritten. Name the files in document_ids in merge order, or bind inputs.sources to a search step's sources output when the files must be found first. kind is required: pdf for PDFs (bookmarks false leaves out the bookmark per file); docx for Word documents (formatting keep_source or use_first, page_breaks, source_headings); pptx for PowerPoint decks (formatting, sections); workbook for CSV or Excel files, one sheet per file (sheets all copies every visible sheet, or sheet names one). Every file must be the kind being merged. To deliver the file, bind the assembly output to render_file with profile assembled_document_v1 and output_format pdf, docx, pptx, or xlsx for workbook. To append the rows of spreadsheets into one table, use tabular_merge instead. A merge of more files than the chat limit, or one that should repeat, belongs in a workflow.", "inputs": { "type": "object", "properties": { - "instruction": { - "type": "string", - "minLength": 1 - }, - "knowledge_basis": { - "type": "string", - "enum": [ + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 2, + "uniqueItems": true, + "description": "The files to merge, in the order they should appear." + }, + "doc_scope": { + "type": "string", + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "kind": { + "type": "string", + "enum": [ + "pdf", + "docx", + "pptx", + "workbook" + ], + "description": "The kind of file to create; every file must be that kind." + }, + "bookmarks": { + "type": "boolean", + "description": "pdf only: a bookmark named after each file; true when omitted." + }, + "formatting": { + "type": "string", + "enum": [ + "keep_source", + "use_first" + ], + "description": "docx and pptx only: keep_source (default) keeps each file's look; use_first uses the first file's styles or theme for all of them." + }, + "page_breaks": { + "type": "boolean", + "description": "docx only: start each document on a new page; true when omitted." + }, + "source_headings": { + "type": "boolean", + "description": "docx only: put a heading with the file name before each document." + }, + "sections": { + "type": "boolean", + "description": "pptx only: group each deck's slides in a section; true when omitted." + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "workbook only: the one sheet to copy from every file." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "workbook only: all copies every visible sheet; first (default) copies one." + } + }, + "required": [ + "kind" + ], + "additionalProperties": false + }, + "cost": "medium", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "document-merge-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ] + }, + "partial_inputs_supported": false, + "result_outputs": { + "assembly": "structured-v1", + "report": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "web_search", + "label": "Search the web", + "role": "gather", + "summary": "Look the question up on the public web.", + "when_to_use": "Use for focused external lookups, current facts, or limited discovery that search results can adequately support. One search can return several sources; that alone does not require deep research. Do not use it to answer questions about the user's own material.", + "inputs": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "query" + ], + "additionalProperties": false + }, + "cost": "low", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "orchestration-gathered-content-v1", + "result_input_kinds": {}, + "partial_inputs_supported": false, + "result_outputs": { + "prepared": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "action_invoke", + "label": "Use an action", + "role": "gather", + "summary": "Gather knowledge using one of this user's accessible actions.", + "when_to_use": "An action in the list reaches the information needed for this task. Prefer using it directly when no agent-specific instructions or knowledge are needed. The step can use the selected action's functions within execution limits. Do not duplicate work delegated to an agent or use this to plan output tasks.", + "inputs": { + "type": "object", + "properties": { + "action_ref": { + "type": "string", + "minLength": 1, + "description": "The exact scoped reference from the actions catalog." + }, + "task": { + "type": "string", + "minLength": 1, + "description": "The knowledge to gather with this action." + }, + "visuals": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "chart" + ] + }, + "uniqueItems": true, + "maxItems": 1, + "description": "Include \"chart\" to have this step chart the exact rows its functions return. The chart is drawn from the retrieved data, not from the prose findings." + } + }, + "required": [ + "action_ref", + "task" + ], + "additionalProperties": false + }, + "cost": "medium", + "produces": [ + "retained_results" + ], + "max_per_plan": null, + "plan_contract_version": 2, + "result_contract_version": "orchestration-gathered-content-v1", + "result_input_kinds": {}, + "partial_inputs_supported": false, + "result_outputs": { + "prepared": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "agent_invoke", + "label": "Ask an agent", + "role": "gather", + "summary": "Hand the task to one of this user's configured agents.", + "when_to_use": "An agent in the list has tools or knowledge built for exactly this task -- reaching a system none of the other capabilities can, or following a procedure somebody configured deliberately. Name only an agent from the list. An agent runs its own tools, so do not also plan the work it would do itself.", + "inputs": { + "type": "object", + "properties": { + "agent_name": { + "type": "string", + "minLength": 1, + "description": "The agent's name, exactly as it appears in the list." + }, + "task": { + "type": "string", + "minLength": 1, + "description": "What to ask the agent to do, in a sentence or two." + }, + "visuals": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "chart", + "diagram", + "image_proposal" + ] + }, + "uniqueItems": true, + "maxItems": 3, + "description": "Visuals the answer will include, so the agent keeps the values, relationships or visual details they need." + } + }, + "required": [ + "agent_name", + "task" + ], + "additionalProperties": false + }, + "cost": "high", + "produces": [ + "retained_results" + ], + "max_per_plan": 1, + "plan_contract_version": 2, + "result_contract_version": "orchestration-gathered-content-v1", + "result_input_kinds": {}, + "partial_inputs_supported": false, + "result_outputs": { + "prepared": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "compose", + "label": "Prepare content", + "role": "reason", + "summary": "Compose reusable text, Markdown, records, or structured content.", + "when_to_use": "Explicitly draft an answer or report, or prepare structured data. Bind every retained input by name. Source-free content is supported. This does not create files, infer a file format, retrieve sources, or call tools. Declare each output and select final_response when its text should become the chat answer. Set knowledge_basis: general_knowledge for stable, widely known facts that need no retrieval; sources when every claim must come from the named inputs (private documents, current or local facts); sources_and_general_knowledge when named inputs lead but stable general knowledge may fill gaps. Mark an input optional only when the answer can still be written from general knowledge if that input fails. Name the visuals the Markdown answer should author: chart, diagram (Mermaid), or image_proposal (cards the user approves before generation). Place bound generate_image outputs with [[image:]] tokens in Markdown, or as \"asset:\" image sources in a prepared slide deck.", + "inputs": { + "type": "object", + "properties": { + "instruction": { + "type": "string", + "minLength": 1 + }, + "knowledge_basis": { + "type": "string", + "enum": [ "general_knowledge", "sources", "sources_and_general_knowledge" @@ -8204,6 +10443,70 @@ } } } + }, + "tabular_column_mapping_v1": { + "type": "object", + "description": "How to line up the columns of several CSV or Excel files before merging them. columns is the merged table's columns in order; use the clearest existing header for each. mappings lists every header whose name differs from the column it fills, and every header to leave out (target_column null). A header that already matches a column, ignoring case and spacing, needs no entry. Map a header only when it holds the same kind of value; when unsure, keep it as its own column or mark confidence low.", + "properties": { + "columns": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 255, + "uniqueItems": true + }, + "mappings": { + "type": "array", + "maxItems": 1024, + "items": { + "type": "object", + "properties": { + "source_column": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "A header exactly as it appears in a file." + }, + "target_column": { + "type": [ + "string", + "null" + ], + "maxLength": 256, + "description": "The column in columns this header fills, or null to leave it out." + }, + "confidence": { + "type": "string", + "enum": [ + "high", + "medium", + "low" + ] + } + }, + "required": [ + "source_column", + "target_column", + "confidence" + ], + "additionalProperties": false + } + }, + "notes": { + "type": "string", + "maxLength": 2000, + "description": "A short explanation of the mapping for the user." + } + }, + "required": [ + "columns", + "mappings" + ], + "additionalProperties": false } }, "capability_availability": { @@ -8212,6 +10515,9 @@ "document_analyze", "document_compare", "tabular_analyze", + "tabular_inspect", + "tabular_merge", + "document_merge", "web_search", "action_invoke", "agent_invoke", @@ -8250,7 +10556,8 @@ "records-v1" ], "profiles": [ - "tabular_records_v1" + "tabular_records_v1", + "exact_tabular_records_v1" ], "embeds_images": false, "status": "unavailable", @@ -8319,10 +10626,13 @@ }, "xlsx": { "source_kinds": [ - "records-v1" + "records-v1", + "structured-v1" ], "profiles": [ - "tabular_workbook_v1" + "tabular_workbook_v1", + "exact_tabular_workbook_v1", + "assembled_document_v1" ], "embeds_images": false, "status": "unavailable", @@ -8331,10 +10641,12 @@ "docx": { "source_kinds": [ "markdown-v1", + "structured-v1", "text-v1" ], "profiles": [ - "prepared_report_v1" + "prepared_report_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -8343,10 +10655,12 @@ "pdf": { "source_kinds": [ "markdown-v1", + "structured-v1", "text-v1" ], "profiles": [ - "prepared_report_v1" + "prepared_report_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -8357,7 +10671,8 @@ "structured-v1" ], "profiles": [ - "prepared_slide_deck_v1" + "prepared_slide_deck_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -9127,16 +11442,385 @@ ] }, { - "id": "web_search", - "label": "Search the web", + "id": "tabular_inspect", + "label": "Inspect spreadsheets", "role": "gather", - "summary": "Look the question up on the public web.", - "when_to_use": "Use for focused external lookups, current facts, or limited discovery that search results can adequately support. One search can return several sources; that alone does not require deep research. Do not use it to answer questions about the user's own material.", + "summary": "Read the sheets, column headers, row counts and a few sample rows of CSV or Excel files, and how their columns line up.", + "when_to_use": "Use before tabular_merge when the files may not share the same columns, or when the user asks what sheets, columns or rows CSV or Excel files have. It reports each file's headers, row counts and sample rows, which files share the same columns, near matches such as \"Customer ID\" and \"customer_id\", a likely title row with the header_row to use, and a suggested schema_policy. A file that cannot be read is reported, not fatal. To line up differently named columns, bind its \"inspection\" output to a compose step that declares a structured-v1 output with profile tabular_column_mapping_v1, and bind that output as tabular_merge's \"mapping\" input. When the user should review the differences first, compose an answer from the inspection and merge in a later turn.", "inputs": { "type": "object", "properties": { - "query": { - "type": "string", + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true, + "description": "The CSV or Excel files to inspect." + }, + "doc_scope": { + "type": "string", + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "One sheet to read in every Excel file. Omit for each workbook's first visible sheet." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "all reads every visible sheet of each workbook; first (default) reads one." + }, + "header_row": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "description": "The row holding the column headers in every file. Omit to use the first nonblank row." + }, + "sample_rows": { + "type": "integer", + "minimum": 0, + "maximum": 10, + "description": "Sample rows to read per sheet; 3 when omitted." + } + }, + "additionalProperties": false + }, + "cost": "low", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "tabular-inspect-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ] + }, + "partial_inputs_supported": false, + "result_outputs": { + "inspection": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "tabular_merge", + "label": "Merge spreadsheets", + "role": "reason", + "summary": "Combine the rows of two or more CSV or Excel files into one table, exactly and without a model, lining up columns by name, alias or a prepared mapping.", + "when_to_use": "Use when the user asks to merge, combine, append, stack or consolidate several CSV or Excel files into one table or file. Every row is kept, in the order the files are listed. It never matches rows on a key column (no joins or lookups) and never edits values; when \"merge\" could mean matching rows on a key, ask. Name the files in document_ids in merge order, or bind inputs.sources to a search step's sources output when the files must be found first. schema_policy: by_name (default) needs the same columns in any order, exact_order also the same order, union keeps every column from every file and leaves a missing one blank, mapped keeps exactly \"columns\". column_aliases lines up other header names, e.g. {\"Customer ID\": [\"cust_id\"]}. When the columns may differ and how they line up is unclear, run tabular_inspect first and bind a compose step's tabular_column_mapping_v1 output as the \"mapping\" input, without columns or aliases. on_incompatible exclude leaves out, and reports, files or sheets that do not fit instead of failing. header_row skips title rows above the headers. sheet picks one sheet; sheets all merges every visible sheet and adds a Source Sheet column. dedupe removes duplicate rows: exact_rows, or key_columns with dedupe_columns, keeping the first or last (dedupe_keep). sort_by orders rows by up to three columns. include_source_column adds each row's file name. To deliver a file, bind the records output to render_file with csv and exact_tabular_records_v1, or xlsx and exact_tabular_workbook_v1. Never compose merged rows. A merge of more files than the chat limit, or one that should repeat, belongs in a workflow.", + "inputs": { + "type": "object", + "properties": { + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 2, + "uniqueItems": true, + "description": "The CSV or Excel files to merge, in the order their rows should appear." + }, + "doc_scope": { + "type": "string", + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "schema_policy": { + "type": "string", + "enum": [ + "by_name", + "exact_order", + "union", + "mapped" + ], + "default": "by_name", + "description": "by_name: same columns in any order. exact_order: same columns in the same order. union: every column from every file. mapped: exactly \"columns\"." + }, + "columns": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 255, + "uniqueItems": true, + "description": "mapped only: the merged columns, in order." + }, + "column_aliases": { + "type": "object", + "maxProperties": 256, + "additionalProperties": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 32, + "uniqueItems": true + }, + "description": "Each column name mapped to the other header names that mean it." + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "One sheet to read in every Excel file. Omit for each workbook's first visible sheet." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "all reads every visible sheet of each workbook; first (default) reads one." + }, + "header_row": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "description": "The row holding the column headers in every file. Omit to use the first nonblank row." + }, + "include_source_column": { + "type": "boolean", + "default": true, + "description": "Add a column holding each row's file name." + }, + "source_column_name": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "default": "Source File" + }, + "on_incompatible": { + "type": "string", + "enum": [ + "fail", + "exclude" + ], + "description": "fail (default) merges nothing when a file does not fit; exclude leaves it out." + }, + "dedupe": { + "type": "string", + "enum": [ + "none", + "exact_rows", + "key_columns" + ], + "description": "Remove duplicate rows; none when omitted." + }, + "dedupe_columns": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 16, + "uniqueItems": true, + "description": "key_columns only: the columns that identify a duplicate." + }, + "dedupe_keep": { + "type": "string", + "enum": [ + "first", + "last" + ], + "description": "Which duplicate to keep; first when omitted." + }, + "sort_by": { + "type": "array", + "maxItems": 3, + "items": { + "type": "object", + "properties": { + "column": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "descending": { + "type": "boolean" + }, + "value_type": { + "type": "string", + "enum": [ + "text", + "number", + "date" + ] + } + }, + "required": [ + "column" + ], + "additionalProperties": false + }, + "description": "Sort keys, most significant first; text ascending when not stated." + } + }, + "additionalProperties": false + }, + "cost": "medium", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "tabular-merge-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ], + "mapping": [ + "structured-v1" + ] + }, + "partial_inputs_supported": false, + "result_outputs": { + "records": "records-v1", + "report": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "document_merge", + "label": "Merge documents", + "role": "reason", + "summary": "Combine two or more PDFs, Word documents or PowerPoint decks into one file of the same kind, or CSV and Excel files into one workbook with a sheet per file, exactly and without a model.", + "when_to_use": "Use when the user asks to combine, join, append, put together or bind several PDFs, Word documents or PowerPoint decks into one file, or to keep several CSV or Excel files as separate sheets of one workbook. Files are copied exactly, in the order listed; nothing is summarized or rewritten. Name the files in document_ids in merge order, or bind inputs.sources to a search step's sources output when the files must be found first. kind is required: pdf for PDFs (bookmarks false leaves out the bookmark per file); docx for Word documents (formatting keep_source or use_first, page_breaks, source_headings); pptx for PowerPoint decks (formatting, sections); workbook for CSV or Excel files, one sheet per file (sheets all copies every visible sheet, or sheet names one). Every file must be the kind being merged. To deliver the file, bind the assembly output to render_file with profile assembled_document_v1 and output_format pdf, docx, pptx, or xlsx for workbook. To append the rows of spreadsheets into one table, use tabular_merge instead. A merge of more files than the chat limit, or one that should repeat, belongs in a workflow.", + "inputs": { + "type": "object", + "properties": { + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 2, + "uniqueItems": true, + "description": "The files to merge, in the order they should appear." + }, + "doc_scope": { + "type": "string", + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "kind": { + "type": "string", + "enum": [ + "pdf", + "docx", + "pptx", + "workbook" + ], + "description": "The kind of file to create; every file must be that kind." + }, + "bookmarks": { + "type": "boolean", + "description": "pdf only: a bookmark named after each file; true when omitted." + }, + "formatting": { + "type": "string", + "enum": [ + "keep_source", + "use_first" + ], + "description": "docx and pptx only: keep_source (default) keeps each file's look; use_first uses the first file's styles or theme for all of them." + }, + "page_breaks": { + "type": "boolean", + "description": "docx only: start each document on a new page; true when omitted." + }, + "source_headings": { + "type": "boolean", + "description": "docx only: put a heading with the file name before each document." + }, + "sections": { + "type": "boolean", + "description": "pptx only: group each deck's slides in a section; true when omitted." + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "workbook only: the one sheet to copy from every file." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "workbook only: all copies every visible sheet; first (default) copies one." + } + }, + "required": [ + "kind" + ], + "additionalProperties": false + }, + "cost": "medium", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "document-merge-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ] + }, + "partial_inputs_supported": false, + "result_outputs": { + "assembly": "structured-v1", + "report": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "web_search", + "label": "Search the web", + "role": "gather", + "summary": "Look the question up on the public web.", + "when_to_use": "Use for focused external lookups, current facts, or limited discovery that search results can adequately support. One search can return several sources; that alone does not require deep research. Do not use it to answer questions about the user's own material.", + "inputs": { + "type": "object", + "properties": { + "query": { + "type": "string", "minLength": 1 } }, @@ -9779,22 +12463,89 @@ } } } - } - }, - "capability_availability": { - "available": [ - "document_search", - "document_analyze", - "document_compare", - "tabular_analyze", - "web_search", - "action_invoke", - "agent_invoke", - "compose", - "workflow_propose", - "workflow_run" - ], - "unavailable": { + }, + "tabular_column_mapping_v1": { + "type": "object", + "description": "How to line up the columns of several CSV or Excel files before merging them. columns is the merged table's columns in order; use the clearest existing header for each. mappings lists every header whose name differs from the column it fills, and every header to leave out (target_column null). A header that already matches a column, ignoring case and spacing, needs no entry. Map a header only when it holds the same kind of value; when unsure, keep it as its own column or mark confidence low.", + "properties": { + "columns": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 255, + "uniqueItems": true + }, + "mappings": { + "type": "array", + "maxItems": 1024, + "items": { + "type": "object", + "properties": { + "source_column": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "A header exactly as it appears in a file." + }, + "target_column": { + "type": [ + "string", + "null" + ], + "maxLength": 256, + "description": "The column in columns this header fills, or null to leave it out." + }, + "confidence": { + "type": "string", + "enum": [ + "high", + "medium", + "low" + ] + } + }, + "required": [ + "source_column", + "target_column", + "confidence" + ], + "additionalProperties": false + } + }, + "notes": { + "type": "string", + "maxLength": 2000, + "description": "A short explanation of the mapping for the user." + } + }, + "required": [ + "columns", + "mappings" + ], + "additionalProperties": false + } + }, + "capability_availability": { + "available": [ + "document_search", + "document_analyze", + "document_compare", + "tabular_analyze", + "tabular_inspect", + "tabular_merge", + "document_merge", + "web_search", + "action_invoke", + "agent_invoke", + "compose", + "workflow_propose", + "workflow_run" + ], + "unavailable": { "url_fetch": "feature_disabled", "deep_research": "feature_disabled", "generate_image": "feature_disabled", @@ -9825,7 +12576,8 @@ "records-v1" ], "profiles": [ - "tabular_records_v1" + "tabular_records_v1", + "exact_tabular_records_v1" ], "embeds_images": false, "status": "unavailable", @@ -9894,10 +12646,13 @@ }, "xlsx": { "source_kinds": [ - "records-v1" + "records-v1", + "structured-v1" ], "profiles": [ - "tabular_workbook_v1" + "tabular_workbook_v1", + "exact_tabular_workbook_v1", + "assembled_document_v1" ], "embeds_images": false, "status": "unavailable", @@ -9906,10 +12661,12 @@ "docx": { "source_kinds": [ "markdown-v1", + "structured-v1", "text-v1" ], "profiles": [ - "prepared_report_v1" + "prepared_report_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -9918,10 +12675,12 @@ "pdf": { "source_kinds": [ "markdown-v1", + "structured-v1", "text-v1" ], "profiles": [ - "prepared_report_v1" + "prepared_report_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -9932,7 +12691,8 @@ "structured-v1" ], "profiles": [ - "prepared_slide_deck_v1" + "prepared_slide_deck_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -10702,22 +13462,60 @@ ] }, { - "id": "web_search", - "label": "Search the web", + "id": "tabular_inspect", + "label": "Inspect spreadsheets", "role": "gather", - "summary": "Look the question up on the public web.", - "when_to_use": "Use for focused external lookups, current facts, or limited discovery that search results can adequately support. One search can return several sources; that alone does not require deep research. Do not use it to answer questions about the user's own material.", + "summary": "Read the sheets, column headers, row counts and a few sample rows of CSV or Excel files, and how their columns line up.", + "when_to_use": "Use before tabular_merge when the files may not share the same columns, or when the user asks what sheets, columns or rows CSV or Excel files have. It reports each file's headers, row counts and sample rows, which files share the same columns, near matches such as \"Customer ID\" and \"customer_id\", a likely title row with the header_row to use, and a suggested schema_policy. A file that cannot be read is reported, not fatal. To line up differently named columns, bind its \"inspection\" output to a compose step that declares a structured-v1 output with profile tabular_column_mapping_v1, and bind that output as tabular_merge's \"mapping\" input. When the user should review the differences first, compose an answer from the inspection and merge in a later turn.", "inputs": { "type": "object", "properties": { - "query": { + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true, + "description": "The CSV or Excel files to inspect." + }, + "doc_scope": { "type": "string", - "minLength": 1 + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "One sheet to read in every Excel file. Omit for each workbook's first visible sheet." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "all reads every visible sheet of each workbook; first (default) reads one." + }, + "header_row": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "description": "The row holding the column headers in every file. Omit to use the first nonblank row." + }, + "sample_rows": { + "type": "integer", + "minimum": 0, + "maximum": 10, + "description": "Sample rows to read per sheet; 3 when omitted." } }, - "required": [ - "query" - ], "additionalProperties": false }, "cost": "low", @@ -10726,137 +13524,468 @@ ], "max_per_plan": 2, "plan_contract_version": 2, - "result_contract_version": "orchestration-gathered-content-v1", - "result_input_kinds": {}, + "result_contract_version": "tabular-inspect-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ] + }, "partial_inputs_supported": false, "result_outputs": { - "prepared": "structured-v1" + "inspection": "structured-v1" }, "optional_result_outputs": {}, "result_output_kinds": [] }, { - "id": "action_invoke", - "label": "Use an action", - "role": "gather", - "summary": "Gather knowledge using one of this user's accessible actions.", - "when_to_use": "An action in the list reaches the information needed for this task. Prefer using it directly when no agent-specific instructions or knowledge are needed. The step can use the selected action's functions within execution limits. Do not duplicate work delegated to an agent or use this to plan output tasks.", + "id": "tabular_merge", + "label": "Merge spreadsheets", + "role": "reason", + "summary": "Combine the rows of two or more CSV or Excel files into one table, exactly and without a model, lining up columns by name, alias or a prepared mapping.", + "when_to_use": "Use when the user asks to merge, combine, append, stack or consolidate several CSV or Excel files into one table or file. Every row is kept, in the order the files are listed. It never matches rows on a key column (no joins or lookups) and never edits values; when \"merge\" could mean matching rows on a key, ask. Name the files in document_ids in merge order, or bind inputs.sources to a search step's sources output when the files must be found first. schema_policy: by_name (default) needs the same columns in any order, exact_order also the same order, union keeps every column from every file and leaves a missing one blank, mapped keeps exactly \"columns\". column_aliases lines up other header names, e.g. {\"Customer ID\": [\"cust_id\"]}. When the columns may differ and how they line up is unclear, run tabular_inspect first and bind a compose step's tabular_column_mapping_v1 output as the \"mapping\" input, without columns or aliases. on_incompatible exclude leaves out, and reports, files or sheets that do not fit instead of failing. header_row skips title rows above the headers. sheet picks one sheet; sheets all merges every visible sheet and adds a Source Sheet column. dedupe removes duplicate rows: exact_rows, or key_columns with dedupe_columns, keeping the first or last (dedupe_keep). sort_by orders rows by up to three columns. include_source_column adds each row's file name. To deliver a file, bind the records output to render_file with csv and exact_tabular_records_v1, or xlsx and exact_tabular_workbook_v1. Never compose merged rows. A merge of more files than the chat limit, or one that should repeat, belongs in a workflow.", "inputs": { "type": "object", "properties": { - "action_ref": { + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 2, + "uniqueItems": true, + "description": "The CSV or Excel files to merge, in the order their rows should appear." + }, + "doc_scope": { "type": "string", - "minLength": 1, - "description": "The exact scoped reference from the actions catalog." + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" }, - "task": { + "schema_policy": { "type": "string", - "minLength": 1, - "description": "The knowledge to gather with this action." + "enum": [ + "by_name", + "exact_order", + "union", + "mapped" + ], + "default": "by_name", + "description": "by_name: same columns in any order. exact_order: same columns in the same order. union: every column from every file. mapped: exactly \"columns\"." }, - "visuals": { + "columns": { "type": "array", "items": { "type": "string", - "enum": [ - "chart" - ] + "minLength": 1, + "maxLength": 256 }, + "minItems": 1, + "maxItems": 255, "uniqueItems": true, - "maxItems": 1, - "description": "Include \"chart\" to have this step chart the exact rows its functions return. The chart is drawn from the retrieved data, not from the prose findings." - } - }, - "required": [ - "action_ref", - "task" - ], - "additionalProperties": false - }, - "cost": "medium", - "produces": [ - "retained_results" - ], - "max_per_plan": null, - "plan_contract_version": 2, - "result_contract_version": "orchestration-gathered-content-v1", - "result_input_kinds": {}, - "partial_inputs_supported": false, - "result_outputs": { - "prepared": "structured-v1" - }, - "optional_result_outputs": {}, - "result_output_kinds": [] - }, - { - "id": "agent_invoke", - "label": "Ask an agent", - "role": "gather", - "summary": "Hand the task to one of this user's configured agents.", - "when_to_use": "An agent in the list has tools or knowledge built for exactly this task -- reaching a system none of the other capabilities can, or following a procedure somebody configured deliberately. Name only an agent from the list. An agent runs its own tools, so do not also plan the work it would do itself.", - "inputs": { - "type": "object", - "properties": { - "agent_name": { + "description": "mapped only: the merged columns, in order." + }, + "column_aliases": { + "type": "object", + "maxProperties": 256, + "additionalProperties": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 32, + "uniqueItems": true + }, + "description": "Each column name mapped to the other header names that mean it." + }, + "sheet": { "type": "string", "minLength": 1, - "description": "The agent's name, exactly as it appears in the list." + "maxLength": 31, + "description": "One sheet to read in every Excel file. Omit for each workbook's first visible sheet." }, - "task": { + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "all reads every visible sheet of each workbook; first (default) reads one." + }, + "header_row": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "description": "The row holding the column headers in every file. Omit to use the first nonblank row." + }, + "include_source_column": { + "type": "boolean", + "default": true, + "description": "Add a column holding each row's file name." + }, + "source_column_name": { "type": "string", "minLength": 1, - "description": "What to ask the agent to do, in a sentence or two." + "maxLength": 128, + "default": "Source File" }, - "visuals": { + "on_incompatible": { + "type": "string", + "enum": [ + "fail", + "exclude" + ], + "description": "fail (default) merges nothing when a file does not fit; exclude leaves it out." + }, + "dedupe": { + "type": "string", + "enum": [ + "none", + "exact_rows", + "key_columns" + ], + "description": "Remove duplicate rows; none when omitted." + }, + "dedupe_columns": { "type": "array", "items": { "type": "string", - "enum": [ - "chart", - "diagram", - "image_proposal" - ] + "minLength": 1, + "maxLength": 256 }, + "minItems": 1, + "maxItems": 16, "uniqueItems": true, + "description": "key_columns only: the columns that identify a duplicate." + }, + "dedupe_keep": { + "type": "string", + "enum": [ + "first", + "last" + ], + "description": "Which duplicate to keep; first when omitted." + }, + "sort_by": { + "type": "array", "maxItems": 3, - "description": "Visuals the answer will include, so the agent keeps the values, relationships or visual details they need." + "items": { + "type": "object", + "properties": { + "column": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "descending": { + "type": "boolean" + }, + "value_type": { + "type": "string", + "enum": [ + "text", + "number", + "date" + ] + } + }, + "required": [ + "column" + ], + "additionalProperties": false + }, + "description": "Sort keys, most significant first; text ascending when not stated." } }, - "required": [ - "agent_name", - "task" - ], "additionalProperties": false }, - "cost": "high", + "cost": "medium", "produces": [ "retained_results" ], - "max_per_plan": 1, + "max_per_plan": 2, "plan_contract_version": 2, - "result_contract_version": "orchestration-gathered-content-v1", - "result_input_kinds": {}, + "result_contract_version": "tabular-merge-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ], + "mapping": [ + "structured-v1" + ] + }, "partial_inputs_supported": false, "result_outputs": { - "prepared": "structured-v1" + "records": "records-v1", + "report": "structured-v1" }, "optional_result_outputs": {}, "result_output_kinds": [] }, { - "id": "compose", - "label": "Prepare content", + "id": "document_merge", + "label": "Merge documents", "role": "reason", - "summary": "Compose reusable text, Markdown, records, or structured content.", - "when_to_use": "Explicitly draft an answer or report, or prepare structured data. Bind every retained input by name. Source-free content is supported. This does not create files, infer a file format, retrieve sources, or call tools. Declare each output and select final_response when its text should become the chat answer. Set knowledge_basis: general_knowledge for stable, widely known facts that need no retrieval; sources when every claim must come from the named inputs (private documents, current or local facts); sources_and_general_knowledge when named inputs lead but stable general knowledge may fill gaps. Mark an input optional only when the answer can still be written from general knowledge if that input fails. Name the visuals the Markdown answer should author: chart, diagram (Mermaid), or image_proposal (cards the user approves before generation). Place bound generate_image outputs with [[image:]] tokens in Markdown, or as \"asset:\" image sources in a prepared slide deck.", + "summary": "Combine two or more PDFs, Word documents or PowerPoint decks into one file of the same kind, or CSV and Excel files into one workbook with a sheet per file, exactly and without a model.", + "when_to_use": "Use when the user asks to combine, join, append, put together or bind several PDFs, Word documents or PowerPoint decks into one file, or to keep several CSV or Excel files as separate sheets of one workbook. Files are copied exactly, in the order listed; nothing is summarized or rewritten. Name the files in document_ids in merge order, or bind inputs.sources to a search step's sources output when the files must be found first. kind is required: pdf for PDFs (bookmarks false leaves out the bookmark per file); docx for Word documents (formatting keep_source or use_first, page_breaks, source_headings); pptx for PowerPoint decks (formatting, sections); workbook for CSV or Excel files, one sheet per file (sheets all copies every visible sheet, or sheet names one). Every file must be the kind being merged. To deliver the file, bind the assembly output to render_file with profile assembled_document_v1 and output_format pdf, docx, pptx, or xlsx for workbook. To append the rows of spreadsheets into one table, use tabular_merge instead. A merge of more files than the chat limit, or one that should repeat, belongs in a workflow.", "inputs": { "type": "object", "properties": { - "instruction": { - "type": "string", - "minLength": 1 + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 2, + "uniqueItems": true, + "description": "The files to merge, in the order they should appear." }, - "knowledge_basis": { + "doc_scope": { + "type": "string", + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "kind": { + "type": "string", + "enum": [ + "pdf", + "docx", + "pptx", + "workbook" + ], + "description": "The kind of file to create; every file must be that kind." + }, + "bookmarks": { + "type": "boolean", + "description": "pdf only: a bookmark named after each file; true when omitted." + }, + "formatting": { + "type": "string", + "enum": [ + "keep_source", + "use_first" + ], + "description": "docx and pptx only: keep_source (default) keeps each file's look; use_first uses the first file's styles or theme for all of them." + }, + "page_breaks": { + "type": "boolean", + "description": "docx only: start each document on a new page; true when omitted." + }, + "source_headings": { + "type": "boolean", + "description": "docx only: put a heading with the file name before each document." + }, + "sections": { + "type": "boolean", + "description": "pptx only: group each deck's slides in a section; true when omitted." + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "workbook only: the one sheet to copy from every file." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "workbook only: all copies every visible sheet; first (default) copies one." + } + }, + "required": [ + "kind" + ], + "additionalProperties": false + }, + "cost": "medium", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "document-merge-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ] + }, + "partial_inputs_supported": false, + "result_outputs": { + "assembly": "structured-v1", + "report": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "web_search", + "label": "Search the web", + "role": "gather", + "summary": "Look the question up on the public web.", + "when_to_use": "Use for focused external lookups, current facts, or limited discovery that search results can adequately support. One search can return several sources; that alone does not require deep research. Do not use it to answer questions about the user's own material.", + "inputs": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "query" + ], + "additionalProperties": false + }, + "cost": "low", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "orchestration-gathered-content-v1", + "result_input_kinds": {}, + "partial_inputs_supported": false, + "result_outputs": { + "prepared": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "action_invoke", + "label": "Use an action", + "role": "gather", + "summary": "Gather knowledge using one of this user's accessible actions.", + "when_to_use": "An action in the list reaches the information needed for this task. Prefer using it directly when no agent-specific instructions or knowledge are needed. The step can use the selected action's functions within execution limits. Do not duplicate work delegated to an agent or use this to plan output tasks.", + "inputs": { + "type": "object", + "properties": { + "action_ref": { + "type": "string", + "minLength": 1, + "description": "The exact scoped reference from the actions catalog." + }, + "task": { + "type": "string", + "minLength": 1, + "description": "The knowledge to gather with this action." + }, + "visuals": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "chart" + ] + }, + "uniqueItems": true, + "maxItems": 1, + "description": "Include \"chart\" to have this step chart the exact rows its functions return. The chart is drawn from the retrieved data, not from the prose findings." + } + }, + "required": [ + "action_ref", + "task" + ], + "additionalProperties": false + }, + "cost": "medium", + "produces": [ + "retained_results" + ], + "max_per_plan": null, + "plan_contract_version": 2, + "result_contract_version": "orchestration-gathered-content-v1", + "result_input_kinds": {}, + "partial_inputs_supported": false, + "result_outputs": { + "prepared": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "agent_invoke", + "label": "Ask an agent", + "role": "gather", + "summary": "Hand the task to one of this user's configured agents.", + "when_to_use": "An agent in the list has tools or knowledge built for exactly this task -- reaching a system none of the other capabilities can, or following a procedure somebody configured deliberately. Name only an agent from the list. An agent runs its own tools, so do not also plan the work it would do itself.", + "inputs": { + "type": "object", + "properties": { + "agent_name": { + "type": "string", + "minLength": 1, + "description": "The agent's name, exactly as it appears in the list." + }, + "task": { + "type": "string", + "minLength": 1, + "description": "What to ask the agent to do, in a sentence or two." + }, + "visuals": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "chart", + "diagram", + "image_proposal" + ] + }, + "uniqueItems": true, + "maxItems": 3, + "description": "Visuals the answer will include, so the agent keeps the values, relationships or visual details they need." + } + }, + "required": [ + "agent_name", + "task" + ], + "additionalProperties": false + }, + "cost": "high", + "produces": [ + "retained_results" + ], + "max_per_plan": 1, + "plan_contract_version": 2, + "result_contract_version": "orchestration-gathered-content-v1", + "result_input_kinds": {}, + "partial_inputs_supported": false, + "result_outputs": { + "prepared": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "compose", + "label": "Prepare content", + "role": "reason", + "summary": "Compose reusable text, Markdown, records, or structured content.", + "when_to_use": "Explicitly draft an answer or report, or prepare structured data. Bind every retained input by name. Source-free content is supported. This does not create files, infer a file format, retrieve sources, or call tools. Declare each output and select final_response when its text should become the chat answer. Set knowledge_basis: general_knowledge for stable, widely known facts that need no retrieval; sources when every claim must come from the named inputs (private documents, current or local facts); sources_and_general_knowledge when named inputs lead but stable general knowledge may fill gaps. Mark an input optional only when the answer can still be written from general knowledge if that input fails. Name the visuals the Markdown answer should author: chart, diagram (Mermaid), or image_proposal (cards the user approves before generation). Place bound generate_image outputs with [[image:]] tokens in Markdown, or as \"asset:\" image sources in a prepared slide deck.", + "inputs": { + "type": "object", + "properties": { + "instruction": { + "type": "string", + "minLength": 1 + }, + "knowledge_basis": { "type": "string", "enum": [ "general_knowledge", @@ -11263,6 +14392,70 @@ } } } + }, + "tabular_column_mapping_v1": { + "type": "object", + "description": "How to line up the columns of several CSV or Excel files before merging them. columns is the merged table's columns in order; use the clearest existing header for each. mappings lists every header whose name differs from the column it fills, and every header to leave out (target_column null). A header that already matches a column, ignoring case and spacing, needs no entry. Map a header only when it holds the same kind of value; when unsure, keep it as its own column or mark confidence low.", + "properties": { + "columns": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 255, + "uniqueItems": true + }, + "mappings": { + "type": "array", + "maxItems": 1024, + "items": { + "type": "object", + "properties": { + "source_column": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "A header exactly as it appears in a file." + }, + "target_column": { + "type": [ + "string", + "null" + ], + "maxLength": 256, + "description": "The column in columns this header fills, or null to leave it out." + }, + "confidence": { + "type": "string", + "enum": [ + "high", + "medium", + "low" + ] + } + }, + "required": [ + "source_column", + "target_column", + "confidence" + ], + "additionalProperties": false + } + }, + "notes": { + "type": "string", + "maxLength": 2000, + "description": "A short explanation of the mapping for the user." + } + }, + "required": [ + "columns", + "mappings" + ], + "additionalProperties": false } }, "capability_availability": { @@ -11271,6 +14464,9 @@ "document_analyze", "document_compare", "tabular_analyze", + "tabular_inspect", + "tabular_merge", + "document_merge", "web_search", "action_invoke", "agent_invoke", @@ -11310,7 +14506,8 @@ "records-v1" ], "profiles": [ - "tabular_records_v1" + "tabular_records_v1", + "exact_tabular_records_v1" ], "embeds_images": false, "status": "unavailable", @@ -11379,10 +14576,13 @@ }, "xlsx": { "source_kinds": [ - "records-v1" + "records-v1", + "structured-v1" ], "profiles": [ - "tabular_workbook_v1" + "tabular_workbook_v1", + "exact_tabular_workbook_v1", + "assembled_document_v1" ], "embeds_images": false, "status": "unavailable", @@ -11391,10 +14591,12 @@ "docx": { "source_kinds": [ "markdown-v1", + "structured-v1", "text-v1" ], "profiles": [ - "prepared_report_v1" + "prepared_report_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -11403,10 +14605,12 @@ "pdf": { "source_kinds": [ "markdown-v1", + "structured-v1", "text-v1" ], "profiles": [ - "prepared_report_v1" + "prepared_report_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -11417,7 +14621,8 @@ "structured-v1" ], "profiles": [ - "prepared_slide_deck_v1" + "prepared_slide_deck_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -11954,18 +15159,387 @@ ] }, { - "id": "web_search", - "label": "Search the web", + "id": "tabular_inspect", + "label": "Inspect spreadsheets", "role": "gather", - "summary": "Look the question up on the public web.", - "when_to_use": "Use for focused external lookups, current facts, or limited discovery that search results can adequately support. One search can return several sources; that alone does not require deep research. Do not use it to answer questions about the user's own material.", + "summary": "Read the sheets, column headers, row counts and a few sample rows of CSV or Excel files, and how their columns line up.", + "when_to_use": "Use before tabular_merge when the files may not share the same columns, or when the user asks what sheets, columns or rows CSV or Excel files have. It reports each file's headers, row counts and sample rows, which files share the same columns, near matches such as \"Customer ID\" and \"customer_id\", a likely title row with the header_row to use, and a suggested schema_policy. A file that cannot be read is reported, not fatal. To line up differently named columns, bind its \"inspection\" output to a compose step that declares a structured-v1 output with profile tabular_column_mapping_v1, and bind that output as tabular_merge's \"mapping\" input. When the user should review the differences first, compose an answer from the inspection and merge in a later turn.", "inputs": { "type": "object", "properties": { - "query": { + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true, + "description": "The CSV or Excel files to inspect." + }, + "doc_scope": { "type": "string", - "minLength": 1 - } + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "One sheet to read in every Excel file. Omit for each workbook's first visible sheet." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "all reads every visible sheet of each workbook; first (default) reads one." + }, + "header_row": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "description": "The row holding the column headers in every file. Omit to use the first nonblank row." + }, + "sample_rows": { + "type": "integer", + "minimum": 0, + "maximum": 10, + "description": "Sample rows to read per sheet; 3 when omitted." + } + }, + "additionalProperties": false + }, + "cost": "low", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "tabular-inspect-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ] + }, + "partial_inputs_supported": false, + "result_outputs": { + "inspection": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "tabular_merge", + "label": "Merge spreadsheets", + "role": "reason", + "summary": "Combine the rows of two or more CSV or Excel files into one table, exactly and without a model, lining up columns by name, alias or a prepared mapping.", + "when_to_use": "Use when the user asks to merge, combine, append, stack or consolidate several CSV or Excel files into one table or file. Every row is kept, in the order the files are listed. It never matches rows on a key column (no joins or lookups) and never edits values; when \"merge\" could mean matching rows on a key, ask. Name the files in document_ids in merge order, or bind inputs.sources to a search step's sources output when the files must be found first. schema_policy: by_name (default) needs the same columns in any order, exact_order also the same order, union keeps every column from every file and leaves a missing one blank, mapped keeps exactly \"columns\". column_aliases lines up other header names, e.g. {\"Customer ID\": [\"cust_id\"]}. When the columns may differ and how they line up is unclear, run tabular_inspect first and bind a compose step's tabular_column_mapping_v1 output as the \"mapping\" input, without columns or aliases. on_incompatible exclude leaves out, and reports, files or sheets that do not fit instead of failing. header_row skips title rows above the headers. sheet picks one sheet; sheets all merges every visible sheet and adds a Source Sheet column. dedupe removes duplicate rows: exact_rows, or key_columns with dedupe_columns, keeping the first or last (dedupe_keep). sort_by orders rows by up to three columns. include_source_column adds each row's file name. To deliver a file, bind the records output to render_file with csv and exact_tabular_records_v1, or xlsx and exact_tabular_workbook_v1. Never compose merged rows. A merge of more files than the chat limit, or one that should repeat, belongs in a workflow.", + "inputs": { + "type": "object", + "properties": { + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 2, + "uniqueItems": true, + "description": "The CSV or Excel files to merge, in the order their rows should appear." + }, + "doc_scope": { + "type": "string", + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "schema_policy": { + "type": "string", + "enum": [ + "by_name", + "exact_order", + "union", + "mapped" + ], + "default": "by_name", + "description": "by_name: same columns in any order. exact_order: same columns in the same order. union: every column from every file. mapped: exactly \"columns\"." + }, + "columns": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 255, + "uniqueItems": true, + "description": "mapped only: the merged columns, in order." + }, + "column_aliases": { + "type": "object", + "maxProperties": 256, + "additionalProperties": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 32, + "uniqueItems": true + }, + "description": "Each column name mapped to the other header names that mean it." + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "One sheet to read in every Excel file. Omit for each workbook's first visible sheet." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "all reads every visible sheet of each workbook; first (default) reads one." + }, + "header_row": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "description": "The row holding the column headers in every file. Omit to use the first nonblank row." + }, + "include_source_column": { + "type": "boolean", + "default": true, + "description": "Add a column holding each row's file name." + }, + "source_column_name": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "default": "Source File" + }, + "on_incompatible": { + "type": "string", + "enum": [ + "fail", + "exclude" + ], + "description": "fail (default) merges nothing when a file does not fit; exclude leaves it out." + }, + "dedupe": { + "type": "string", + "enum": [ + "none", + "exact_rows", + "key_columns" + ], + "description": "Remove duplicate rows; none when omitted." + }, + "dedupe_columns": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 16, + "uniqueItems": true, + "description": "key_columns only: the columns that identify a duplicate." + }, + "dedupe_keep": { + "type": "string", + "enum": [ + "first", + "last" + ], + "description": "Which duplicate to keep; first when omitted." + }, + "sort_by": { + "type": "array", + "maxItems": 3, + "items": { + "type": "object", + "properties": { + "column": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "descending": { + "type": "boolean" + }, + "value_type": { + "type": "string", + "enum": [ + "text", + "number", + "date" + ] + } + }, + "required": [ + "column" + ], + "additionalProperties": false + }, + "description": "Sort keys, most significant first; text ascending when not stated." + } + }, + "additionalProperties": false + }, + "cost": "medium", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "tabular-merge-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ], + "mapping": [ + "structured-v1" + ] + }, + "partial_inputs_supported": false, + "result_outputs": { + "records": "records-v1", + "report": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "document_merge", + "label": "Merge documents", + "role": "reason", + "summary": "Combine two or more PDFs, Word documents or PowerPoint decks into one file of the same kind, or CSV and Excel files into one workbook with a sheet per file, exactly and without a model.", + "when_to_use": "Use when the user asks to combine, join, append, put together or bind several PDFs, Word documents or PowerPoint decks into one file, or to keep several CSV or Excel files as separate sheets of one workbook. Files are copied exactly, in the order listed; nothing is summarized or rewritten. Name the files in document_ids in merge order, or bind inputs.sources to a search step's sources output when the files must be found first. kind is required: pdf for PDFs (bookmarks false leaves out the bookmark per file); docx for Word documents (formatting keep_source or use_first, page_breaks, source_headings); pptx for PowerPoint decks (formatting, sections); workbook for CSV or Excel files, one sheet per file (sheets all copies every visible sheet, or sheet names one). Every file must be the kind being merged. To deliver the file, bind the assembly output to render_file with profile assembled_document_v1 and output_format pdf, docx, pptx, or xlsx for workbook. To append the rows of spreadsheets into one table, use tabular_merge instead. A merge of more files than the chat limit, or one that should repeat, belongs in a workflow.", + "inputs": { + "type": "object", + "properties": { + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 2, + "uniqueItems": true, + "description": "The files to merge, in the order they should appear." + }, + "doc_scope": { + "type": "string", + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "kind": { + "type": "string", + "enum": [ + "pdf", + "docx", + "pptx", + "workbook" + ], + "description": "The kind of file to create; every file must be that kind." + }, + "bookmarks": { + "type": "boolean", + "description": "pdf only: a bookmark named after each file; true when omitted." + }, + "formatting": { + "type": "string", + "enum": [ + "keep_source", + "use_first" + ], + "description": "docx and pptx only: keep_source (default) keeps each file's look; use_first uses the first file's styles or theme for all of them." + }, + "page_breaks": { + "type": "boolean", + "description": "docx only: start each document on a new page; true when omitted." + }, + "source_headings": { + "type": "boolean", + "description": "docx only: put a heading with the file name before each document." + }, + "sections": { + "type": "boolean", + "description": "pptx only: group each deck's slides in a section; true when omitted." + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "workbook only: the one sheet to copy from every file." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "workbook only: all copies every visible sheet; first (default) copies one." + } + }, + "required": [ + "kind" + ], + "additionalProperties": false + }, + "cost": "medium", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "document-merge-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ] + }, + "partial_inputs_supported": false, + "result_outputs": { + "assembly": "structured-v1", + "report": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "web_search", + "label": "Search the web", + "role": "gather", + "summary": "Look the question up on the public web.", + "when_to_use": "Use for focused external lookups, current facts, or limited discovery that search results can adequately support. One search can return several sources; that alone does not require deep research. Do not use it to answer questions about the user's own material.", + "inputs": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1 + } }, "required": [ "query" @@ -12515,31 +16089,98 @@ } } } - } - }, - "capability_availability": { - "available": [ - "document_search", - "document_analyze", - "document_compare", - "tabular_analyze", - "web_search", - "action_invoke", - "agent_invoke", - "compose" - ], - "unavailable": { - "url_fetch": "feature_disabled", - "deep_research": "feature_disabled", - "generate_image": "feature_disabled", - "workflow_propose": "workflow_shared_conversation", - "workflow_run": "workflow_shared_conversation", - "workflow_results": "workflow_shared_conversation", - "render_file": "export_catalog_unavailable" }, - "web_discovery_enabled": true, - "visual_outputs": { - "charts": true, + "tabular_column_mapping_v1": { + "type": "object", + "description": "How to line up the columns of several CSV or Excel files before merging them. columns is the merged table's columns in order; use the clearest existing header for each. mappings lists every header whose name differs from the column it fills, and every header to leave out (target_column null). A header that already matches a column, ignoring case and spacing, needs no entry. Map a header only when it holds the same kind of value; when unsure, keep it as its own column or mark confidence low.", + "properties": { + "columns": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 255, + "uniqueItems": true + }, + "mappings": { + "type": "array", + "maxItems": 1024, + "items": { + "type": "object", + "properties": { + "source_column": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "A header exactly as it appears in a file." + }, + "target_column": { + "type": [ + "string", + "null" + ], + "maxLength": 256, + "description": "The column in columns this header fills, or null to leave it out." + }, + "confidence": { + "type": "string", + "enum": [ + "high", + "medium", + "low" + ] + } + }, + "required": [ + "source_column", + "target_column", + "confidence" + ], + "additionalProperties": false + } + }, + "notes": { + "type": "string", + "maxLength": 2000, + "description": "A short explanation of the mapping for the user." + } + }, + "required": [ + "columns", + "mappings" + ], + "additionalProperties": false + } + }, + "capability_availability": { + "available": [ + "document_search", + "document_analyze", + "document_compare", + "tabular_analyze", + "tabular_inspect", + "tabular_merge", + "document_merge", + "web_search", + "action_invoke", + "agent_invoke", + "compose" + ], + "unavailable": { + "url_fetch": "feature_disabled", + "deep_research": "feature_disabled", + "generate_image": "feature_disabled", + "workflow_propose": "workflow_shared_conversation", + "workflow_run": "workflow_shared_conversation", + "workflow_results": "workflow_shared_conversation", + "render_file": "export_catalog_unavailable" + }, + "web_discovery_enabled": true, + "visual_outputs": { + "charts": true, "diagrams": true, "image_proposals": false }, @@ -12562,7 +16203,8 @@ "records-v1" ], "profiles": [ - "tabular_records_v1" + "tabular_records_v1", + "exact_tabular_records_v1" ], "embeds_images": false, "status": "unavailable", @@ -12631,10 +16273,13 @@ }, "xlsx": { "source_kinds": [ - "records-v1" + "records-v1", + "structured-v1" ], "profiles": [ - "tabular_workbook_v1" + "tabular_workbook_v1", + "exact_tabular_workbook_v1", + "assembled_document_v1" ], "embeds_images": false, "status": "unavailable", @@ -12643,10 +16288,12 @@ "docx": { "source_kinds": [ "markdown-v1", + "structured-v1", "text-v1" ], "profiles": [ - "prepared_report_v1" + "prepared_report_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -12655,10 +16302,12 @@ "pdf": { "source_kinds": [ "markdown-v1", + "structured-v1", "text-v1" ], "profiles": [ - "prepared_report_v1" + "prepared_report_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -12669,7 +16318,8 @@ "structured-v1" ], "profiles": [ - "prepared_slide_deck_v1" + "prepared_slide_deck_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -13374,6 +17024,70 @@ } } } + }, + "tabular_column_mapping_v1": { + "type": "object", + "description": "How to line up the columns of several CSV or Excel files before merging them. columns is the merged table's columns in order; use the clearest existing header for each. mappings lists every header whose name differs from the column it fills, and every header to leave out (target_column null). A header that already matches a column, ignoring case and spacing, needs no entry. Map a header only when it holds the same kind of value; when unsure, keep it as its own column or mark confidence low.", + "properties": { + "columns": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 255, + "uniqueItems": true + }, + "mappings": { + "type": "array", + "maxItems": 1024, + "items": { + "type": "object", + "properties": { + "source_column": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "A header exactly as it appears in a file." + }, + "target_column": { + "type": [ + "string", + "null" + ], + "maxLength": 256, + "description": "The column in columns this header fills, or null to leave it out." + }, + "confidence": { + "type": "string", + "enum": [ + "high", + "medium", + "low" + ] + } + }, + "required": [ + "source_column", + "target_column", + "confidence" + ], + "additionalProperties": false + } + }, + "notes": { + "type": "string", + "maxLength": 2000, + "description": "A short explanation of the mapping for the user." + } + }, + "required": [ + "columns", + "mappings" + ], + "additionalProperties": false } }, "capability_availability": { @@ -13388,6 +17102,9 @@ "document_search": "not_enabled_for_orchestration", "document_compare": "not_enabled_for_orchestration", "tabular_analyze": "not_enabled_for_orchestration", + "tabular_inspect": "not_enabled_for_orchestration", + "tabular_merge": "not_enabled_for_orchestration", + "document_merge": "not_enabled_for_orchestration", "web_search": "not_enabled_for_orchestration", "url_fetch": "not_enabled_for_orchestration", "deep_research": "not_enabled_for_orchestration", @@ -13421,7 +17138,8 @@ "records-v1" ], "profiles": [ - "tabular_records_v1" + "tabular_records_v1", + "exact_tabular_records_v1" ], "embeds_images": false, "status": "unavailable", @@ -13490,10 +17208,13 @@ }, "xlsx": { "source_kinds": [ - "records-v1" + "records-v1", + "structured-v1" ], "profiles": [ - "tabular_workbook_v1" + "tabular_workbook_v1", + "exact_tabular_workbook_v1", + "assembled_document_v1" ], "embeds_images": false, "status": "unavailable", @@ -13502,10 +17223,12 @@ "docx": { "source_kinds": [ "markdown-v1", + "structured-v1", "text-v1" ], "profiles": [ - "prepared_report_v1" + "prepared_report_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -13514,10 +17237,12 @@ "pdf": { "source_kinds": [ "markdown-v1", + "structured-v1", "text-v1" ], "profiles": [ - "prepared_report_v1" + "prepared_report_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -13528,7 +17253,8 @@ "structured-v1" ], "profiles": [ - "prepared_slide_deck_v1" + "prepared_slide_deck_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -14298,22 +18024,60 @@ ] }, { - "id": "web_search", - "label": "Search the web", + "id": "tabular_inspect", + "label": "Inspect spreadsheets", "role": "gather", - "summary": "Look the question up on the public web.", - "when_to_use": "Use for focused external lookups, current facts, or limited discovery that search results can adequately support. One search can return several sources; that alone does not require deep research. Do not use it to answer questions about the user's own material.", + "summary": "Read the sheets, column headers, row counts and a few sample rows of CSV or Excel files, and how their columns line up.", + "when_to_use": "Use before tabular_merge when the files may not share the same columns, or when the user asks what sheets, columns or rows CSV or Excel files have. It reports each file's headers, row counts and sample rows, which files share the same columns, near matches such as \"Customer ID\" and \"customer_id\", a likely title row with the header_row to use, and a suggested schema_policy. A file that cannot be read is reported, not fatal. To line up differently named columns, bind its \"inspection\" output to a compose step that declares a structured-v1 output with profile tabular_column_mapping_v1, and bind that output as tabular_merge's \"mapping\" input. When the user should review the differences first, compose an answer from the inspection and merge in a later turn.", "inputs": { "type": "object", "properties": { - "query": { + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true, + "description": "The CSV or Excel files to inspect." + }, + "doc_scope": { "type": "string", - "minLength": 1 + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "One sheet to read in every Excel file. Omit for each workbook's first visible sheet." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "all reads every visible sheet of each workbook; first (default) reads one." + }, + "header_row": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "description": "The row holding the column headers in every file. Omit to use the first nonblank row." + }, + "sample_rows": { + "type": "integer", + "minimum": 0, + "maximum": 10, + "description": "Sample rows to read per sheet; 3 when omitted." } }, - "required": [ - "query" - ], "additionalProperties": false }, "cost": "low", @@ -14322,81 +18086,412 @@ ], "max_per_plan": 2, "plan_contract_version": 2, - "result_contract_version": "orchestration-gathered-content-v1", - "result_input_kinds": {}, + "result_contract_version": "tabular-inspect-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ] + }, "partial_inputs_supported": false, "result_outputs": { - "prepared": "structured-v1" + "inspection": "structured-v1" }, "optional_result_outputs": {}, "result_output_kinds": [] }, { - "id": "action_invoke", - "label": "Use an action", - "role": "gather", - "summary": "Gather knowledge using one of this user's accessible actions.", - "when_to_use": "An action in the list reaches the information needed for this task. Prefer using it directly when no agent-specific instructions or knowledge are needed. The step can use the selected action's functions within execution limits. Do not duplicate work delegated to an agent or use this to plan output tasks.", + "id": "tabular_merge", + "label": "Merge spreadsheets", + "role": "reason", + "summary": "Combine the rows of two or more CSV or Excel files into one table, exactly and without a model, lining up columns by name, alias or a prepared mapping.", + "when_to_use": "Use when the user asks to merge, combine, append, stack or consolidate several CSV or Excel files into one table or file. Every row is kept, in the order the files are listed. It never matches rows on a key column (no joins or lookups) and never edits values; when \"merge\" could mean matching rows on a key, ask. Name the files in document_ids in merge order, or bind inputs.sources to a search step's sources output when the files must be found first. schema_policy: by_name (default) needs the same columns in any order, exact_order also the same order, union keeps every column from every file and leaves a missing one blank, mapped keeps exactly \"columns\". column_aliases lines up other header names, e.g. {\"Customer ID\": [\"cust_id\"]}. When the columns may differ and how they line up is unclear, run tabular_inspect first and bind a compose step's tabular_column_mapping_v1 output as the \"mapping\" input, without columns or aliases. on_incompatible exclude leaves out, and reports, files or sheets that do not fit instead of failing. header_row skips title rows above the headers. sheet picks one sheet; sheets all merges every visible sheet and adds a Source Sheet column. dedupe removes duplicate rows: exact_rows, or key_columns with dedupe_columns, keeping the first or last (dedupe_keep). sort_by orders rows by up to three columns. include_source_column adds each row's file name. To deliver a file, bind the records output to render_file with csv and exact_tabular_records_v1, or xlsx and exact_tabular_workbook_v1. Never compose merged rows. A merge of more files than the chat limit, or one that should repeat, belongs in a workflow.", "inputs": { "type": "object", "properties": { - "action_ref": { + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 2, + "uniqueItems": true, + "description": "The CSV or Excel files to merge, in the order their rows should appear." + }, + "doc_scope": { + "type": "string", + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "schema_policy": { + "type": "string", + "enum": [ + "by_name", + "exact_order", + "union", + "mapped" + ], + "default": "by_name", + "description": "by_name: same columns in any order. exact_order: same columns in the same order. union: every column from every file. mapped: exactly \"columns\"." + }, + "columns": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 255, + "uniqueItems": true, + "description": "mapped only: the merged columns, in order." + }, + "column_aliases": { + "type": "object", + "maxProperties": 256, + "additionalProperties": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 32, + "uniqueItems": true + }, + "description": "Each column name mapped to the other header names that mean it." + }, + "sheet": { "type": "string", "minLength": 1, - "description": "The exact scoped reference from the actions catalog." + "maxLength": 31, + "description": "One sheet to read in every Excel file. Omit for each workbook's first visible sheet." }, - "task": { + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "all reads every visible sheet of each workbook; first (default) reads one." + }, + "header_row": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "description": "The row holding the column headers in every file. Omit to use the first nonblank row." + }, + "include_source_column": { + "type": "boolean", + "default": true, + "description": "Add a column holding each row's file name." + }, + "source_column_name": { "type": "string", "minLength": 1, - "description": "The knowledge to gather with this action." + "maxLength": 128, + "default": "Source File" }, - "visuals": { + "on_incompatible": { + "type": "string", + "enum": [ + "fail", + "exclude" + ], + "description": "fail (default) merges nothing when a file does not fit; exclude leaves it out." + }, + "dedupe": { + "type": "string", + "enum": [ + "none", + "exact_rows", + "key_columns" + ], + "description": "Remove duplicate rows; none when omitted." + }, + "dedupe_columns": { "type": "array", "items": { "type": "string", - "enum": [ - "chart" - ] + "minLength": 1, + "maxLength": 256 }, + "minItems": 1, + "maxItems": 16, "uniqueItems": true, - "maxItems": 1, - "description": "Include \"chart\" to have this step chart the exact rows its functions return. The chart is drawn from the retrieved data, not from the prose findings." + "description": "key_columns only: the columns that identify a duplicate." + }, + "dedupe_keep": { + "type": "string", + "enum": [ + "first", + "last" + ], + "description": "Which duplicate to keep; first when omitted." + }, + "sort_by": { + "type": "array", + "maxItems": 3, + "items": { + "type": "object", + "properties": { + "column": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "descending": { + "type": "boolean" + }, + "value_type": { + "type": "string", + "enum": [ + "text", + "number", + "date" + ] + } + }, + "required": [ + "column" + ], + "additionalProperties": false + }, + "description": "Sort keys, most significant first; text ascending when not stated." } }, - "required": [ - "action_ref", - "task" - ], "additionalProperties": false }, "cost": "medium", "produces": [ "retained_results" ], - "max_per_plan": null, + "max_per_plan": 2, "plan_contract_version": 2, - "result_contract_version": "orchestration-gathered-content-v1", - "result_input_kinds": {}, + "result_contract_version": "tabular-merge-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ], + "mapping": [ + "structured-v1" + ] + }, "partial_inputs_supported": false, "result_outputs": { - "prepared": "structured-v1" + "records": "records-v1", + "report": "structured-v1" }, "optional_result_outputs": {}, "result_output_kinds": [] }, { - "id": "agent_invoke", - "label": "Ask an agent", - "role": "gather", - "summary": "Hand the task to one of this user's configured agents.", - "when_to_use": "An agent in the list has tools or knowledge built for exactly this task -- reaching a system none of the other capabilities can, or following a procedure somebody configured deliberately. Name only an agent from the list. An agent runs its own tools, so do not also plan the work it would do itself.", + "id": "document_merge", + "label": "Merge documents", + "role": "reason", + "summary": "Combine two or more PDFs, Word documents or PowerPoint decks into one file of the same kind, or CSV and Excel files into one workbook with a sheet per file, exactly and without a model.", + "when_to_use": "Use when the user asks to combine, join, append, put together or bind several PDFs, Word documents or PowerPoint decks into one file, or to keep several CSV or Excel files as separate sheets of one workbook. Files are copied exactly, in the order listed; nothing is summarized or rewritten. Name the files in document_ids in merge order, or bind inputs.sources to a search step's sources output when the files must be found first. kind is required: pdf for PDFs (bookmarks false leaves out the bookmark per file); docx for Word documents (formatting keep_source or use_first, page_breaks, source_headings); pptx for PowerPoint decks (formatting, sections); workbook for CSV or Excel files, one sheet per file (sheets all copies every visible sheet, or sheet names one). Every file must be the kind being merged. To deliver the file, bind the assembly output to render_file with profile assembled_document_v1 and output_format pdf, docx, pptx, or xlsx for workbook. To append the rows of spreadsheets into one table, use tabular_merge instead. A merge of more files than the chat limit, or one that should repeat, belongs in a workflow.", "inputs": { "type": "object", "properties": { - "agent_name": { - "type": "string", - "minLength": 1, - "description": "The agent's name, exactly as it appears in the list." + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 2, + "uniqueItems": true, + "description": "The files to merge, in the order they should appear." + }, + "doc_scope": { + "type": "string", + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "kind": { + "type": "string", + "enum": [ + "pdf", + "docx", + "pptx", + "workbook" + ], + "description": "The kind of file to create; every file must be that kind." + }, + "bookmarks": { + "type": "boolean", + "description": "pdf only: a bookmark named after each file; true when omitted." + }, + "formatting": { + "type": "string", + "enum": [ + "keep_source", + "use_first" + ], + "description": "docx and pptx only: keep_source (default) keeps each file's look; use_first uses the first file's styles or theme for all of them." + }, + "page_breaks": { + "type": "boolean", + "description": "docx only: start each document on a new page; true when omitted." + }, + "source_headings": { + "type": "boolean", + "description": "docx only: put a heading with the file name before each document." + }, + "sections": { + "type": "boolean", + "description": "pptx only: group each deck's slides in a section; true when omitted." + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "workbook only: the one sheet to copy from every file." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "workbook only: all copies every visible sheet; first (default) copies one." + } + }, + "required": [ + "kind" + ], + "additionalProperties": false + }, + "cost": "medium", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "document-merge-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ] + }, + "partial_inputs_supported": false, + "result_outputs": { + "assembly": "structured-v1", + "report": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "web_search", + "label": "Search the web", + "role": "gather", + "summary": "Look the question up on the public web.", + "when_to_use": "Use for focused external lookups, current facts, or limited discovery that search results can adequately support. One search can return several sources; that alone does not require deep research. Do not use it to answer questions about the user's own material.", + "inputs": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "query" + ], + "additionalProperties": false + }, + "cost": "low", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "orchestration-gathered-content-v1", + "result_input_kinds": {}, + "partial_inputs_supported": false, + "result_outputs": { + "prepared": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "action_invoke", + "label": "Use an action", + "role": "gather", + "summary": "Gather knowledge using one of this user's accessible actions.", + "when_to_use": "An action in the list reaches the information needed for this task. Prefer using it directly when no agent-specific instructions or knowledge are needed. The step can use the selected action's functions within execution limits. Do not duplicate work delegated to an agent or use this to plan output tasks.", + "inputs": { + "type": "object", + "properties": { + "action_ref": { + "type": "string", + "minLength": 1, + "description": "The exact scoped reference from the actions catalog." + }, + "task": { + "type": "string", + "minLength": 1, + "description": "The knowledge to gather with this action." + }, + "visuals": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "chart" + ] + }, + "uniqueItems": true, + "maxItems": 1, + "description": "Include \"chart\" to have this step chart the exact rows its functions return. The chart is drawn from the retrieved data, not from the prose findings." + } + }, + "required": [ + "action_ref", + "task" + ], + "additionalProperties": false + }, + "cost": "medium", + "produces": [ + "retained_results" + ], + "max_per_plan": null, + "plan_contract_version": 2, + "result_contract_version": "orchestration-gathered-content-v1", + "result_input_kinds": {}, + "partial_inputs_supported": false, + "result_outputs": { + "prepared": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "agent_invoke", + "label": "Ask an agent", + "role": "gather", + "summary": "Hand the task to one of this user's configured agents.", + "when_to_use": "An agent in the list has tools or knowledge built for exactly this task -- reaching a system none of the other capabilities can, or following a procedure somebody configured deliberately. Name only an agent from the list. An agent runs its own tools, so do not also plan the work it would do itself.", + "inputs": { + "type": "object", + "properties": { + "agent_name": { + "type": "string", + "minLength": 1, + "description": "The agent's name, exactly as it appears in the list." }, "task": { "type": "string", @@ -14951,6 +19046,70 @@ } } } + }, + "tabular_column_mapping_v1": { + "type": "object", + "description": "How to line up the columns of several CSV or Excel files before merging them. columns is the merged table's columns in order; use the clearest existing header for each. mappings lists every header whose name differs from the column it fills, and every header to leave out (target_column null). A header that already matches a column, ignoring case and spacing, needs no entry. Map a header only when it holds the same kind of value; when unsure, keep it as its own column or mark confidence low.", + "properties": { + "columns": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 255, + "uniqueItems": true + }, + "mappings": { + "type": "array", + "maxItems": 1024, + "items": { + "type": "object", + "properties": { + "source_column": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "A header exactly as it appears in a file." + }, + "target_column": { + "type": [ + "string", + "null" + ], + "maxLength": 256, + "description": "The column in columns this header fills, or null to leave it out." + }, + "confidence": { + "type": "string", + "enum": [ + "high", + "medium", + "low" + ] + } + }, + "required": [ + "source_column", + "target_column", + "confidence" + ], + "additionalProperties": false + } + }, + "notes": { + "type": "string", + "maxLength": 2000, + "description": "A short explanation of the mapping for the user." + } + }, + "required": [ + "columns", + "mappings" + ], + "additionalProperties": false } }, "capability_availability": { @@ -14959,6 +19118,9 @@ "document_analyze", "document_compare", "tabular_analyze", + "tabular_inspect", + "tabular_merge", + "document_merge", "web_search", "action_invoke", "agent_invoke", @@ -14998,7 +19160,8 @@ "records-v1" ], "profiles": [ - "tabular_records_v1" + "tabular_records_v1", + "exact_tabular_records_v1" ], "embeds_images": false, "status": "unavailable", @@ -15067,10 +19230,13 @@ }, "xlsx": { "source_kinds": [ - "records-v1" + "records-v1", + "structured-v1" ], "profiles": [ - "tabular_workbook_v1" + "tabular_workbook_v1", + "exact_tabular_workbook_v1", + "assembled_document_v1" ], "embeds_images": false, "status": "unavailable", @@ -15079,10 +19245,12 @@ "docx": { "source_kinds": [ "markdown-v1", + "structured-v1", "text-v1" ], "profiles": [ - "prepared_report_v1" + "prepared_report_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -15091,10 +19259,12 @@ "pdf": { "source_kinds": [ "markdown-v1", + "structured-v1", "text-v1" ], "profiles": [ - "prepared_report_v1" + "prepared_report_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -15105,7 +19275,8 @@ "structured-v1" ], "profiles": [ - "prepared_slide_deck_v1" + "prepared_slide_deck_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -15818,22 +19989,60 @@ ] }, { - "id": "web_search", - "label": "Search the web", + "id": "tabular_inspect", + "label": "Inspect spreadsheets", "role": "gather", - "summary": "Look the question up on the public web.", - "when_to_use": "Use for focused external lookups, current facts, or limited discovery that search results can adequately support. One search can return several sources; that alone does not require deep research. Do not use it to answer questions about the user's own material.", + "summary": "Read the sheets, column headers, row counts and a few sample rows of CSV or Excel files, and how their columns line up.", + "when_to_use": "Use before tabular_merge when the files may not share the same columns, or when the user asks what sheets, columns or rows CSV or Excel files have. It reports each file's headers, row counts and sample rows, which files share the same columns, near matches such as \"Customer ID\" and \"customer_id\", a likely title row with the header_row to use, and a suggested schema_policy. A file that cannot be read is reported, not fatal. To line up differently named columns, bind its \"inspection\" output to a compose step that declares a structured-v1 output with profile tabular_column_mapping_v1, and bind that output as tabular_merge's \"mapping\" input. When the user should review the differences first, compose an answer from the inspection and merge in a later turn.", "inputs": { "type": "object", "properties": { - "query": { + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true, + "description": "The CSV or Excel files to inspect." + }, + "doc_scope": { "type": "string", - "minLength": 1 + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "One sheet to read in every Excel file. Omit for each workbook's first visible sheet." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "all reads every visible sheet of each workbook; first (default) reads one." + }, + "header_row": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "description": "The row holding the column headers in every file. Omit to use the first nonblank row." + }, + "sample_rows": { + "type": "integer", + "minimum": 0, + "maximum": 10, + "description": "Sample rows to read per sheet; 3 when omitted." } }, - "required": [ - "query" - ], "additionalProperties": false }, "cost": "low", @@ -15842,38 +20051,369 @@ ], "max_per_plan": 2, "plan_contract_version": 2, - "result_contract_version": "orchestration-gathered-content-v1", - "result_input_kinds": {}, + "result_contract_version": "tabular-inspect-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ] + }, "partial_inputs_supported": false, "result_outputs": { - "prepared": "structured-v1" + "inspection": "structured-v1" }, "optional_result_outputs": {}, "result_output_kinds": [] }, { - "id": "action_invoke", - "label": "Use an action", - "role": "gather", - "summary": "Gather knowledge using one of this user's accessible actions.", - "when_to_use": "An action in the list reaches the information needed for this task. Prefer using it directly when no agent-specific instructions or knowledge are needed. The step can use the selected action's functions within execution limits. Do not duplicate work delegated to an agent or use this to plan output tasks.", + "id": "tabular_merge", + "label": "Merge spreadsheets", + "role": "reason", + "summary": "Combine the rows of two or more CSV or Excel files into one table, exactly and without a model, lining up columns by name, alias or a prepared mapping.", + "when_to_use": "Use when the user asks to merge, combine, append, stack or consolidate several CSV or Excel files into one table or file. Every row is kept, in the order the files are listed. It never matches rows on a key column (no joins or lookups) and never edits values; when \"merge\" could mean matching rows on a key, ask. Name the files in document_ids in merge order, or bind inputs.sources to a search step's sources output when the files must be found first. schema_policy: by_name (default) needs the same columns in any order, exact_order also the same order, union keeps every column from every file and leaves a missing one blank, mapped keeps exactly \"columns\". column_aliases lines up other header names, e.g. {\"Customer ID\": [\"cust_id\"]}. When the columns may differ and how they line up is unclear, run tabular_inspect first and bind a compose step's tabular_column_mapping_v1 output as the \"mapping\" input, without columns or aliases. on_incompatible exclude leaves out, and reports, files or sheets that do not fit instead of failing. header_row skips title rows above the headers. sheet picks one sheet; sheets all merges every visible sheet and adds a Source Sheet column. dedupe removes duplicate rows: exact_rows, or key_columns with dedupe_columns, keeping the first or last (dedupe_keep). sort_by orders rows by up to three columns. include_source_column adds each row's file name. To deliver a file, bind the records output to render_file with csv and exact_tabular_records_v1, or xlsx and exact_tabular_workbook_v1. Never compose merged rows. A merge of more files than the chat limit, or one that should repeat, belongs in a workflow.", "inputs": { "type": "object", "properties": { - "action_ref": { - "type": "string", - "minLength": 1, - "description": "The exact scoped reference from the actions catalog." - }, - "task": { - "type": "string", - "minLength": 1, - "description": "The knowledge to gather with this action." - }, - "visuals": { + "document_ids": { "type": "array", "items": { - "type": "string", + "type": "string" + }, + "minItems": 2, + "uniqueItems": true, + "description": "The CSV or Excel files to merge, in the order their rows should appear." + }, + "doc_scope": { + "type": "string", + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "schema_policy": { + "type": "string", + "enum": [ + "by_name", + "exact_order", + "union", + "mapped" + ], + "default": "by_name", + "description": "by_name: same columns in any order. exact_order: same columns in the same order. union: every column from every file. mapped: exactly \"columns\"." + }, + "columns": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 255, + "uniqueItems": true, + "description": "mapped only: the merged columns, in order." + }, + "column_aliases": { + "type": "object", + "maxProperties": 256, + "additionalProperties": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 32, + "uniqueItems": true + }, + "description": "Each column name mapped to the other header names that mean it." + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "One sheet to read in every Excel file. Omit for each workbook's first visible sheet." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "all reads every visible sheet of each workbook; first (default) reads one." + }, + "header_row": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "description": "The row holding the column headers in every file. Omit to use the first nonblank row." + }, + "include_source_column": { + "type": "boolean", + "default": true, + "description": "Add a column holding each row's file name." + }, + "source_column_name": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "default": "Source File" + }, + "on_incompatible": { + "type": "string", + "enum": [ + "fail", + "exclude" + ], + "description": "fail (default) merges nothing when a file does not fit; exclude leaves it out." + }, + "dedupe": { + "type": "string", + "enum": [ + "none", + "exact_rows", + "key_columns" + ], + "description": "Remove duplicate rows; none when omitted." + }, + "dedupe_columns": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 16, + "uniqueItems": true, + "description": "key_columns only: the columns that identify a duplicate." + }, + "dedupe_keep": { + "type": "string", + "enum": [ + "first", + "last" + ], + "description": "Which duplicate to keep; first when omitted." + }, + "sort_by": { + "type": "array", + "maxItems": 3, + "items": { + "type": "object", + "properties": { + "column": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "descending": { + "type": "boolean" + }, + "value_type": { + "type": "string", + "enum": [ + "text", + "number", + "date" + ] + } + }, + "required": [ + "column" + ], + "additionalProperties": false + }, + "description": "Sort keys, most significant first; text ascending when not stated." + } + }, + "additionalProperties": false + }, + "cost": "medium", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "tabular-merge-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ], + "mapping": [ + "structured-v1" + ] + }, + "partial_inputs_supported": false, + "result_outputs": { + "records": "records-v1", + "report": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "document_merge", + "label": "Merge documents", + "role": "reason", + "summary": "Combine two or more PDFs, Word documents or PowerPoint decks into one file of the same kind, or CSV and Excel files into one workbook with a sheet per file, exactly and without a model.", + "when_to_use": "Use when the user asks to combine, join, append, put together or bind several PDFs, Word documents or PowerPoint decks into one file, or to keep several CSV or Excel files as separate sheets of one workbook. Files are copied exactly, in the order listed; nothing is summarized or rewritten. Name the files in document_ids in merge order, or bind inputs.sources to a search step's sources output when the files must be found first. kind is required: pdf for PDFs (bookmarks false leaves out the bookmark per file); docx for Word documents (formatting keep_source or use_first, page_breaks, source_headings); pptx for PowerPoint decks (formatting, sections); workbook for CSV or Excel files, one sheet per file (sheets all copies every visible sheet, or sheet names one). Every file must be the kind being merged. To deliver the file, bind the assembly output to render_file with profile assembled_document_v1 and output_format pdf, docx, pptx, or xlsx for workbook. To append the rows of spreadsheets into one table, use tabular_merge instead. A merge of more files than the chat limit, or one that should repeat, belongs in a workflow.", + "inputs": { + "type": "object", + "properties": { + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 2, + "uniqueItems": true, + "description": "The files to merge, in the order they should appear." + }, + "doc_scope": { + "type": "string", + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "kind": { + "type": "string", + "enum": [ + "pdf", + "docx", + "pptx", + "workbook" + ], + "description": "The kind of file to create; every file must be that kind." + }, + "bookmarks": { + "type": "boolean", + "description": "pdf only: a bookmark named after each file; true when omitted." + }, + "formatting": { + "type": "string", + "enum": [ + "keep_source", + "use_first" + ], + "description": "docx and pptx only: keep_source (default) keeps each file's look; use_first uses the first file's styles or theme for all of them." + }, + "page_breaks": { + "type": "boolean", + "description": "docx only: start each document on a new page; true when omitted." + }, + "source_headings": { + "type": "boolean", + "description": "docx only: put a heading with the file name before each document." + }, + "sections": { + "type": "boolean", + "description": "pptx only: group each deck's slides in a section; true when omitted." + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "workbook only: the one sheet to copy from every file." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "workbook only: all copies every visible sheet; first (default) copies one." + } + }, + "required": [ + "kind" + ], + "additionalProperties": false + }, + "cost": "medium", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "document-merge-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ] + }, + "partial_inputs_supported": false, + "result_outputs": { + "assembly": "structured-v1", + "report": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "web_search", + "label": "Search the web", + "role": "gather", + "summary": "Look the question up on the public web.", + "when_to_use": "Use for focused external lookups, current facts, or limited discovery that search results can adequately support. One search can return several sources; that alone does not require deep research. Do not use it to answer questions about the user's own material.", + "inputs": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "query" + ], + "additionalProperties": false + }, + "cost": "low", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "orchestration-gathered-content-v1", + "result_input_kinds": {}, + "partial_inputs_supported": false, + "result_outputs": { + "prepared": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "action_invoke", + "label": "Use an action", + "role": "gather", + "summary": "Gather knowledge using one of this user's accessible actions.", + "when_to_use": "An action in the list reaches the information needed for this task. Prefer using it directly when no agent-specific instructions or knowledge are needed. The step can use the selected action's functions within execution limits. Do not duplicate work delegated to an agent or use this to plan output tasks.", + "inputs": { + "type": "object", + "properties": { + "action_ref": { + "type": "string", + "minLength": 1, + "description": "The exact scoped reference from the actions catalog." + }, + "task": { + "type": "string", + "minLength": 1, + "description": "The knowledge to gather with this action." + }, + "visuals": { + "type": "array", + "items": { + "type": "string", "enum": [ "chart" ] @@ -16471,35 +21011,102 @@ } } } - } - }, - "capability_availability": { - "available": [ - "document_search", - "document_analyze", - "document_compare", - "tabular_analyze", - "web_search", - "action_invoke", - "agent_invoke", - "compose", - "workflow_run", - "workflow_results" - ], - "unavailable": { - "url_fetch": "feature_disabled", - "deep_research": "feature_disabled", - "generate_image": "feature_disabled", - "workflow_propose": "workflow_context_unavailable", - "render_file": "export_catalog_unavailable" - }, - "web_discovery_enabled": true, - "visual_outputs": { - "charts": true, - "diagrams": true, - "image_proposals": false }, - "deliverables": { + "tabular_column_mapping_v1": { + "type": "object", + "description": "How to line up the columns of several CSV or Excel files before merging them. columns is the merged table's columns in order; use the clearest existing header for each. mappings lists every header whose name differs from the column it fills, and every header to leave out (target_column null). A header that already matches a column, ignoring case and spacing, needs no entry. Map a header only when it holds the same kind of value; when unsure, keep it as its own column or mark confidence low.", + "properties": { + "columns": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 255, + "uniqueItems": true + }, + "mappings": { + "type": "array", + "maxItems": 1024, + "items": { + "type": "object", + "properties": { + "source_column": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "A header exactly as it appears in a file." + }, + "target_column": { + "type": [ + "string", + "null" + ], + "maxLength": 256, + "description": "The column in columns this header fills, or null to leave it out." + }, + "confidence": { + "type": "string", + "enum": [ + "high", + "medium", + "low" + ] + } + }, + "required": [ + "source_column", + "target_column", + "confidence" + ], + "additionalProperties": false + } + }, + "notes": { + "type": "string", + "maxLength": 2000, + "description": "A short explanation of the mapping for the user." + } + }, + "required": [ + "columns", + "mappings" + ], + "additionalProperties": false + } + }, + "capability_availability": { + "available": [ + "document_search", + "document_analyze", + "document_compare", + "tabular_analyze", + "tabular_inspect", + "tabular_merge", + "document_merge", + "web_search", + "action_invoke", + "agent_invoke", + "compose", + "workflow_run", + "workflow_results" + ], + "unavailable": { + "url_fetch": "feature_disabled", + "deep_research": "feature_disabled", + "generate_image": "feature_disabled", + "workflow_propose": "workflow_context_unavailable", + "render_file": "export_catalog_unavailable" + }, + "web_discovery_enabled": true, + "visual_outputs": { + "charts": true, + "diagrams": true, + "image_proposals": false + }, + "deliverables": { "answer": { "status": "available", "produced_by": [ @@ -16518,7 +21125,8 @@ "records-v1" ], "profiles": [ - "tabular_records_v1" + "tabular_records_v1", + "exact_tabular_records_v1" ], "embeds_images": false, "status": "unavailable", @@ -16587,10 +21195,13 @@ }, "xlsx": { "source_kinds": [ - "records-v1" + "records-v1", + "structured-v1" ], "profiles": [ - "tabular_workbook_v1" + "tabular_workbook_v1", + "exact_tabular_workbook_v1", + "assembled_document_v1" ], "embeds_images": false, "status": "unavailable", @@ -16599,10 +21210,12 @@ "docx": { "source_kinds": [ "markdown-v1", + "structured-v1", "text-v1" ], "profiles": [ - "prepared_report_v1" + "prepared_report_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -16611,10 +21224,12 @@ "pdf": { "source_kinds": [ "markdown-v1", + "structured-v1", "text-v1" ], "profiles": [ - "prepared_report_v1" + "prepared_report_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -16625,7 +21240,8 @@ "structured-v1" ], "profiles": [ - "prepared_slide_deck_v1" + "prepared_slide_deck_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -17255,7 +21871,436 @@ ] } } - ] + ] + }, + "cost": "medium", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "native-tabular-result-v1", + "result_input_kinds": {}, + "partial_inputs_supported": false, + "result_outputs": { + "coverage": "structured-v1" + }, + "optional_result_outputs": { + "records": "records-v1", + "analysis": "structured-v1" + }, + "result_output_kinds": [], + "result_output_variants": [ + { + "native_operation": "query", + "task_type": "structured_export", + "outputs": [ + { + "name": "records", + "kind": "records-v1" + }, + { + "name": "coverage", + "kind": "structured-v1" + } + ] + }, + { + "native_operation": "transform", + "task_type": "structured_export", + "outputs": [ + { + "name": "records", + "kind": "records-v1" + }, + { + "name": "coverage", + "kind": "structured-v1" + } + ] + }, + { + "native_operation": "analysis", + "task_type": "hierarchical_analysis", + "outputs": [ + { + "name": "analysis", + "kind": "structured-v1" + }, + { + "name": "coverage", + "kind": "structured-v1" + } + ] + }, + { + "native_operation": "transform", + "task_type": "combined", + "outputs": [ + { + "name": "records", + "kind": "records-v1" + }, + { + "name": "analysis", + "kind": "structured-v1" + }, + { + "name": "coverage", + "kind": "structured-v1" + } + ] + } + ] + }, + { + "id": "tabular_inspect", + "label": "Inspect spreadsheets", + "role": "gather", + "summary": "Read the sheets, column headers, row counts and a few sample rows of CSV or Excel files, and how their columns line up.", + "when_to_use": "Use before tabular_merge when the files may not share the same columns, or when the user asks what sheets, columns or rows CSV or Excel files have. It reports each file's headers, row counts and sample rows, which files share the same columns, near matches such as \"Customer ID\" and \"customer_id\", a likely title row with the header_row to use, and a suggested schema_policy. A file that cannot be read is reported, not fatal. To line up differently named columns, bind its \"inspection\" output to a compose step that declares a structured-v1 output with profile tabular_column_mapping_v1, and bind that output as tabular_merge's \"mapping\" input. When the user should review the differences first, compose an answer from the inspection and merge in a later turn.", + "inputs": { + "type": "object", + "properties": { + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true, + "description": "The CSV or Excel files to inspect." + }, + "doc_scope": { + "type": "string", + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "One sheet to read in every Excel file. Omit for each workbook's first visible sheet." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "all reads every visible sheet of each workbook; first (default) reads one." + }, + "header_row": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "description": "The row holding the column headers in every file. Omit to use the first nonblank row." + }, + "sample_rows": { + "type": "integer", + "minimum": 0, + "maximum": 10, + "description": "Sample rows to read per sheet; 3 when omitted." + } + }, + "additionalProperties": false + }, + "cost": "low", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "tabular-inspect-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ] + }, + "partial_inputs_supported": false, + "result_outputs": { + "inspection": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "tabular_merge", + "label": "Merge spreadsheets", + "role": "reason", + "summary": "Combine the rows of two or more CSV or Excel files into one table, exactly and without a model, lining up columns by name, alias or a prepared mapping.", + "when_to_use": "Use when the user asks to merge, combine, append, stack or consolidate several CSV or Excel files into one table or file. Every row is kept, in the order the files are listed. It never matches rows on a key column (no joins or lookups) and never edits values; when \"merge\" could mean matching rows on a key, ask. Name the files in document_ids in merge order, or bind inputs.sources to a search step's sources output when the files must be found first. schema_policy: by_name (default) needs the same columns in any order, exact_order also the same order, union keeps every column from every file and leaves a missing one blank, mapped keeps exactly \"columns\". column_aliases lines up other header names, e.g. {\"Customer ID\": [\"cust_id\"]}. When the columns may differ and how they line up is unclear, run tabular_inspect first and bind a compose step's tabular_column_mapping_v1 output as the \"mapping\" input, without columns or aliases. on_incompatible exclude leaves out, and reports, files or sheets that do not fit instead of failing. header_row skips title rows above the headers. sheet picks one sheet; sheets all merges every visible sheet and adds a Source Sheet column. dedupe removes duplicate rows: exact_rows, or key_columns with dedupe_columns, keeping the first or last (dedupe_keep). sort_by orders rows by up to three columns. include_source_column adds each row's file name. To deliver a file, bind the records output to render_file with csv and exact_tabular_records_v1, or xlsx and exact_tabular_workbook_v1. Never compose merged rows. A merge of more files than the chat limit, or one that should repeat, belongs in a workflow.", + "inputs": { + "type": "object", + "properties": { + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 2, + "uniqueItems": true, + "description": "The CSV or Excel files to merge, in the order their rows should appear." + }, + "doc_scope": { + "type": "string", + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "schema_policy": { + "type": "string", + "enum": [ + "by_name", + "exact_order", + "union", + "mapped" + ], + "default": "by_name", + "description": "by_name: same columns in any order. exact_order: same columns in the same order. union: every column from every file. mapped: exactly \"columns\"." + }, + "columns": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 255, + "uniqueItems": true, + "description": "mapped only: the merged columns, in order." + }, + "column_aliases": { + "type": "object", + "maxProperties": 256, + "additionalProperties": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 32, + "uniqueItems": true + }, + "description": "Each column name mapped to the other header names that mean it." + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "One sheet to read in every Excel file. Omit for each workbook's first visible sheet." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "all reads every visible sheet of each workbook; first (default) reads one." + }, + "header_row": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "description": "The row holding the column headers in every file. Omit to use the first nonblank row." + }, + "include_source_column": { + "type": "boolean", + "default": true, + "description": "Add a column holding each row's file name." + }, + "source_column_name": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "default": "Source File" + }, + "on_incompatible": { + "type": "string", + "enum": [ + "fail", + "exclude" + ], + "description": "fail (default) merges nothing when a file does not fit; exclude leaves it out." + }, + "dedupe": { + "type": "string", + "enum": [ + "none", + "exact_rows", + "key_columns" + ], + "description": "Remove duplicate rows; none when omitted." + }, + "dedupe_columns": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 16, + "uniqueItems": true, + "description": "key_columns only: the columns that identify a duplicate." + }, + "dedupe_keep": { + "type": "string", + "enum": [ + "first", + "last" + ], + "description": "Which duplicate to keep; first when omitted." + }, + "sort_by": { + "type": "array", + "maxItems": 3, + "items": { + "type": "object", + "properties": { + "column": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "descending": { + "type": "boolean" + }, + "value_type": { + "type": "string", + "enum": [ + "text", + "number", + "date" + ] + } + }, + "required": [ + "column" + ], + "additionalProperties": false + }, + "description": "Sort keys, most significant first; text ascending when not stated." + } + }, + "additionalProperties": false + }, + "cost": "medium", + "produces": [ + "retained_results" + ], + "max_per_plan": 2, + "plan_contract_version": 2, + "result_contract_version": "tabular-merge-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ], + "mapping": [ + "structured-v1" + ] + }, + "partial_inputs_supported": false, + "result_outputs": { + "records": "records-v1", + "report": "structured-v1" + }, + "optional_result_outputs": {}, + "result_output_kinds": [] + }, + { + "id": "document_merge", + "label": "Merge documents", + "role": "reason", + "summary": "Combine two or more PDFs, Word documents or PowerPoint decks into one file of the same kind, or CSV and Excel files into one workbook with a sheet per file, exactly and without a model.", + "when_to_use": "Use when the user asks to combine, join, append, put together or bind several PDFs, Word documents or PowerPoint decks into one file, or to keep several CSV or Excel files as separate sheets of one workbook. Files are copied exactly, in the order listed; nothing is summarized or rewritten. Name the files in document_ids in merge order, or bind inputs.sources to a search step's sources output when the files must be found first. kind is required: pdf for PDFs (bookmarks false leaves out the bookmark per file); docx for Word documents (formatting keep_source or use_first, page_breaks, source_headings); pptx for PowerPoint decks (formatting, sections); workbook for CSV or Excel files, one sheet per file (sheets all copies every visible sheet, or sheet names one). Every file must be the kind being merged. To deliver the file, bind the assembly output to render_file with profile assembled_document_v1 and output_format pdf, docx, pptx, or xlsx for workbook. To append the rows of spreadsheets into one table, use tabular_merge instead. A merge of more files than the chat limit, or one that should repeat, belongs in a workflow.", + "inputs": { + "type": "object", + "properties": { + "document_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 2, + "uniqueItems": true, + "description": "The files to merge, in the order they should appear." + }, + "doc_scope": { + "type": "string", + "enum": [ + "all", + "personal", + "group", + "public" + ], + "default": "all" + }, + "kind": { + "type": "string", + "enum": [ + "pdf", + "docx", + "pptx", + "workbook" + ], + "description": "The kind of file to create; every file must be that kind." + }, + "bookmarks": { + "type": "boolean", + "description": "pdf only: a bookmark named after each file; true when omitted." + }, + "formatting": { + "type": "string", + "enum": [ + "keep_source", + "use_first" + ], + "description": "docx and pptx only: keep_source (default) keeps each file's look; use_first uses the first file's styles or theme for all of them." + }, + "page_breaks": { + "type": "boolean", + "description": "docx only: start each document on a new page; true when omitted." + }, + "source_headings": { + "type": "boolean", + "description": "docx only: put a heading with the file name before each document." + }, + "sections": { + "type": "boolean", + "description": "pptx only: group each deck's slides in a section; true when omitted." + }, + "sheet": { + "type": "string", + "minLength": 1, + "maxLength": 31, + "description": "workbook only: the one sheet to copy from every file." + }, + "sheets": { + "type": "string", + "enum": [ + "first", + "all" + ], + "description": "workbook only: all copies every visible sheet; first (default) copies one." + } + }, + "required": [ + "kind" + ], + "additionalProperties": false }, "cost": "medium", "produces": [ @@ -17263,79 +22308,19 @@ ], "max_per_plan": 2, "plan_contract_version": 2, - "result_contract_version": "native-tabular-result-v1", - "result_input_kinds": {}, + "result_contract_version": "document-merge-v1", + "result_input_kinds": { + "sources": [ + "source-set-v1" + ] + }, "partial_inputs_supported": false, "result_outputs": { - "coverage": "structured-v1" - }, - "optional_result_outputs": { - "records": "records-v1", - "analysis": "structured-v1" + "assembly": "structured-v1", + "report": "structured-v1" }, - "result_output_kinds": [], - "result_output_variants": [ - { - "native_operation": "query", - "task_type": "structured_export", - "outputs": [ - { - "name": "records", - "kind": "records-v1" - }, - { - "name": "coverage", - "kind": "structured-v1" - } - ] - }, - { - "native_operation": "transform", - "task_type": "structured_export", - "outputs": [ - { - "name": "records", - "kind": "records-v1" - }, - { - "name": "coverage", - "kind": "structured-v1" - } - ] - }, - { - "native_operation": "analysis", - "task_type": "hierarchical_analysis", - "outputs": [ - { - "name": "analysis", - "kind": "structured-v1" - }, - { - "name": "coverage", - "kind": "structured-v1" - } - ] - }, - { - "native_operation": "transform", - "task_type": "combined", - "outputs": [ - { - "name": "records", - "kind": "records-v1" - }, - { - "name": "analysis", - "kind": "structured-v1" - }, - { - "name": "coverage", - "kind": "structured-v1" - } - ] - } - ] + "optional_result_outputs": {}, + "result_output_kinds": [] }, { "id": "web_search", @@ -17899,6 +22884,70 @@ } } } + }, + "tabular_column_mapping_v1": { + "type": "object", + "description": "How to line up the columns of several CSV or Excel files before merging them. columns is the merged table's columns in order; use the clearest existing header for each. mappings lists every header whose name differs from the column it fills, and every header to leave out (target_column null). A header that already matches a column, ignoring case and spacing, needs no entry. Map a header only when it holds the same kind of value; when unsure, keep it as its own column or mark confidence low.", + "properties": { + "columns": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 255, + "uniqueItems": true + }, + "mappings": { + "type": "array", + "maxItems": 1024, + "items": { + "type": "object", + "properties": { + "source_column": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "A header exactly as it appears in a file." + }, + "target_column": { + "type": [ + "string", + "null" + ], + "maxLength": 256, + "description": "The column in columns this header fills, or null to leave it out." + }, + "confidence": { + "type": "string", + "enum": [ + "high", + "medium", + "low" + ] + } + }, + "required": [ + "source_column", + "target_column", + "confidence" + ], + "additionalProperties": false + } + }, + "notes": { + "type": "string", + "maxLength": 2000, + "description": "A short explanation of the mapping for the user." + } + }, + "required": [ + "columns", + "mappings" + ], + "additionalProperties": false } }, "capability_availability": { @@ -17907,6 +22956,9 @@ "document_analyze", "document_compare", "tabular_analyze", + "tabular_inspect", + "tabular_merge", + "document_merge", "web_search", "action_invoke", "agent_invoke", @@ -17946,7 +22998,8 @@ "records-v1" ], "profiles": [ - "tabular_records_v1" + "tabular_records_v1", + "exact_tabular_records_v1" ], "embeds_images": false, "status": "unavailable", @@ -18015,10 +23068,13 @@ }, "xlsx": { "source_kinds": [ - "records-v1" + "records-v1", + "structured-v1" ], "profiles": [ - "tabular_workbook_v1" + "tabular_workbook_v1", + "exact_tabular_workbook_v1", + "assembled_document_v1" ], "embeds_images": false, "status": "unavailable", @@ -18027,10 +23083,12 @@ "docx": { "source_kinds": [ "markdown-v1", + "structured-v1", "text-v1" ], "profiles": [ - "prepared_report_v1" + "prepared_report_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -18039,10 +23097,12 @@ "pdf": { "source_kinds": [ "markdown-v1", + "structured-v1", "text-v1" ], "profiles": [ - "prepared_report_v1" + "prepared_report_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -18053,7 +23113,8 @@ "structured-v1" ], "profiles": [ - "prepared_slide_deck_v1" + "prepared_slide_deck_v1", + "assembled_document_v1" ], "embeds_images": true, "status": "unavailable", @@ -18757,6 +23818,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -18772,6 +23836,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -18820,6 +23887,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -18835,6 +23905,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -18899,6 +23972,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -18914,6 +23990,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -19114,6 +24193,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -19129,6 +24211,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -19321,6 +24406,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -19336,6 +24424,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -19389,6 +24480,9 @@ "document_analyze", "document_compare", "tabular_analyze", + "tabular_inspect", + "tabular_merge", + "document_merge", "web_search", "action_invoke", "agent_invoke", @@ -19435,6 +24529,27 @@ "summary": "Compute over CSV or Excel data rather than reading it as prose.", "cost": "medium" }, + { + "id": "tabular_inspect", + "label": "Inspect spreadsheets", + "role": "gather", + "summary": "Read the sheets, column headers, row counts and a few sample rows of CSV or Excel files, and how their columns line up.", + "cost": "low" + }, + { + "id": "tabular_merge", + "label": "Merge spreadsheets", + "role": "reason", + "summary": "Combine the rows of two or more CSV or Excel files into one table, exactly and without a model, lining up columns by name, alias or a prepared mapping.", + "cost": "medium" + }, + { + "id": "document_merge", + "label": "Merge documents", + "role": "reason", + "summary": "Combine two or more PDFs, Word documents or PowerPoint decks into one file of the same kind, or CSV and Excel files into one workbook with a sheet per file, exactly and without a model.", + "cost": "medium" + }, { "id": "web_search", "label": "Search the web", @@ -19622,6 +24737,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_agent_invoke": true, "available_compose": true, @@ -19637,6 +24755,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_agent_invoke": false, "required_compose": false, @@ -19685,6 +24806,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_agent_invoke": true, "available_compose": true, @@ -19700,6 +24824,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_agent_invoke": false, "required_compose": false, @@ -19764,6 +24891,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_agent_invoke": true, "available_compose": true, @@ -19779,6 +24909,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_agent_invoke": false, "required_compose": false, @@ -19818,6 +24951,9 @@ "document_analyze", "document_compare", "tabular_analyze", + "tabular_inspect", + "tabular_merge", + "document_merge", "web_search", "agent_invoke", "compose" @@ -19864,6 +25000,27 @@ "summary": "Compute over CSV or Excel data rather than reading it as prose.", "cost": "medium" }, + { + "id": "tabular_inspect", + "label": "Inspect spreadsheets", + "role": "gather", + "summary": "Read the sheets, column headers, row counts and a few sample rows of CSV or Excel files, and how their columns line up.", + "cost": "low" + }, + { + "id": "tabular_merge", + "label": "Merge spreadsheets", + "role": "reason", + "summary": "Combine the rows of two or more CSV or Excel files into one table, exactly and without a model, lining up columns by name, alias or a prepared mapping.", + "cost": "medium" + }, + { + "id": "document_merge", + "label": "Merge documents", + "role": "reason", + "summary": "Combine two or more PDFs, Word documents or PowerPoint decks into one file of the same kind, or CSV and Excel files into one workbook with a sheet per file, exactly and without a model.", + "cost": "medium" + }, { "id": "web_search", "label": "Search the web", @@ -20023,6 +25180,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -20038,6 +25198,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -20086,6 +25249,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -20101,6 +25267,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -20165,6 +25334,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -20180,6 +25352,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -20219,6 +25394,9 @@ "document_analyze", "document_compare", "tabular_analyze", + "tabular_inspect", + "tabular_merge", + "document_merge", "web_search", "action_invoke", "agent_invoke", @@ -20265,6 +25443,27 @@ "summary": "Compute over CSV or Excel data rather than reading it as prose.", "cost": "medium" }, + { + "id": "tabular_inspect", + "label": "Inspect spreadsheets", + "role": "gather", + "summary": "Read the sheets, column headers, row counts and a few sample rows of CSV or Excel files, and how their columns line up.", + "cost": "low" + }, + { + "id": "tabular_merge", + "label": "Merge spreadsheets", + "role": "reason", + "summary": "Combine the rows of two or more CSV or Excel files into one table, exactly and without a model, lining up columns by name, alias or a prepared mapping.", + "cost": "medium" + }, + { + "id": "document_merge", + "label": "Merge documents", + "role": "reason", + "summary": "Combine two or more PDFs, Word documents or PowerPoint decks into one file of the same kind, or CSV and Excel files into one workbook with a sheet per file, exactly and without a model.", + "cost": "medium" + }, { "id": "web_search", "label": "Search the web", @@ -20669,6 +25868,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -20683,6 +25885,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -20730,6 +25935,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -20744,6 +25952,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -20807,6 +26018,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -20821,6 +26035,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -20859,6 +26076,9 @@ "document_analyze", "document_compare", "tabular_analyze", + "tabular_inspect", + "tabular_merge", + "document_merge", "web_search", "action_invoke", "agent_invoke", @@ -20904,6 +26124,27 @@ "summary": "Compute over CSV or Excel data rather than reading it as prose.", "cost": "medium" }, + { + "id": "tabular_inspect", + "label": "Inspect spreadsheets", + "role": "gather", + "summary": "Read the sheets, column headers, row counts and a few sample rows of CSV or Excel files, and how their columns line up.", + "cost": "low" + }, + { + "id": "tabular_merge", + "label": "Merge spreadsheets", + "role": "reason", + "summary": "Combine the rows of two or more CSV or Excel files into one table, exactly and without a model, lining up columns by name, alias or a prepared mapping.", + "cost": "medium" + }, + { + "id": "document_merge", + "label": "Merge documents", + "role": "reason", + "summary": "Combine two or more PDFs, Word documents or PowerPoint decks into one file of the same kind, or CSV and Excel files into one workbook with a sheet per file, exactly and without a model.", + "cost": "medium" + }, { "id": "web_search", "label": "Search the web", @@ -21589,6 +26830,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -21603,6 +26847,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -21650,6 +26897,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -21664,6 +26914,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -21727,6 +26980,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -21741,6 +26997,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -21779,6 +27038,9 @@ "document_analyze", "document_compare", "tabular_analyze", + "tabular_inspect", + "tabular_merge", + "document_merge", "web_search", "action_invoke", "agent_invoke", @@ -21824,6 +27086,27 @@ "summary": "Compute over CSV or Excel data rather than reading it as prose.", "cost": "medium" }, + { + "id": "tabular_inspect", + "label": "Inspect spreadsheets", + "role": "gather", + "summary": "Read the sheets, column headers, row counts and a few sample rows of CSV or Excel files, and how their columns line up.", + "cost": "low" + }, + { + "id": "tabular_merge", + "label": "Merge spreadsheets", + "role": "reason", + "summary": "Combine the rows of two or more CSV or Excel files into one table, exactly and without a model, lining up columns by name, alias or a prepared mapping.", + "cost": "medium" + }, + { + "id": "document_merge", + "label": "Merge documents", + "role": "reason", + "summary": "Combine two or more PDFs, Word documents or PowerPoint decks into one file of the same kind, or CSV and Excel files into one workbook with a sheet per file, exactly and without a model.", + "cost": "medium" + }, { "id": "web_search", "label": "Search the web", @@ -22509,6 +27792,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -22523,6 +27809,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -22570,6 +27859,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -22584,6 +27876,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -22647,6 +27942,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -22661,6 +27959,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -22699,6 +28000,9 @@ "document_analyze", "document_compare", "tabular_analyze", + "tabular_inspect", + "tabular_merge", + "document_merge", "web_search", "action_invoke", "agent_invoke", @@ -22744,6 +28048,27 @@ "summary": "Compute over CSV or Excel data rather than reading it as prose.", "cost": "medium" }, + { + "id": "tabular_inspect", + "label": "Inspect spreadsheets", + "role": "gather", + "summary": "Read the sheets, column headers, row counts and a few sample rows of CSV or Excel files, and how their columns line up.", + "cost": "low" + }, + { + "id": "tabular_merge", + "label": "Merge spreadsheets", + "role": "reason", + "summary": "Combine the rows of two or more CSV or Excel files into one table, exactly and without a model, lining up columns by name, alias or a prepared mapping.", + "cost": "medium" + }, + { + "id": "document_merge", + "label": "Merge documents", + "role": "reason", + "summary": "Combine two or more PDFs, Word documents or PowerPoint decks into one file of the same kind, or CSV and Excel files into one workbook with a sheet per file, exactly and without a model.", + "cost": "medium" + }, { "id": "web_search", "label": "Search the web", @@ -22924,6 +28249,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -22939,6 +28267,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -22987,6 +28318,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -23002,6 +28336,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -23066,6 +28403,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -23081,6 +28421,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -23120,6 +28463,9 @@ "document_analyze", "document_compare", "tabular_analyze", + "tabular_inspect", + "tabular_merge", + "document_merge", "web_search", "action_invoke", "agent_invoke", @@ -23166,6 +28512,27 @@ "summary": "Compute over CSV or Excel data rather than reading it as prose.", "cost": "medium" }, + { + "id": "tabular_inspect", + "label": "Inspect spreadsheets", + "role": "gather", + "summary": "Read the sheets, column headers, row counts and a few sample rows of CSV or Excel files, and how their columns line up.", + "cost": "low" + }, + { + "id": "tabular_merge", + "label": "Merge spreadsheets", + "role": "reason", + "summary": "Combine the rows of two or more CSV or Excel files into one table, exactly and without a model, lining up columns by name, alias or a prepared mapping.", + "cost": "medium" + }, + { + "id": "document_merge", + "label": "Merge documents", + "role": "reason", + "summary": "Combine two or more PDFs, Word documents or PowerPoint decks into one file of the same kind, or CSV and Excel files into one workbook with a sheet per file, exactly and without a model.", + "cost": "medium" + }, { "id": "web_search", "label": "Search the web", @@ -23353,6 +28720,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -23368,6 +28738,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -23416,6 +28789,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -23431,6 +28807,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -23495,6 +28874,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -23510,6 +28892,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -23549,6 +28934,9 @@ "document_analyze", "document_compare", "tabular_analyze", + "tabular_inspect", + "tabular_merge", + "document_merge", "web_search", "action_invoke", "agent_invoke", @@ -23595,6 +28983,27 @@ "summary": "Compute over CSV or Excel data rather than reading it as prose.", "cost": "medium" }, + { + "id": "tabular_inspect", + "label": "Inspect spreadsheets", + "role": "gather", + "summary": "Read the sheets, column headers, row counts and a few sample rows of CSV or Excel files, and how their columns line up.", + "cost": "low" + }, + { + "id": "tabular_merge", + "label": "Merge spreadsheets", + "role": "reason", + "summary": "Combine the rows of two or more CSV or Excel files into one table, exactly and without a model, lining up columns by name, alias or a prepared mapping.", + "cost": "medium" + }, + { + "id": "document_merge", + "label": "Merge documents", + "role": "reason", + "summary": "Combine two or more PDFs, Word documents or PowerPoint decks into one file of the same kind, or CSV and Excel files into one workbook with a sheet per file, exactly and without a model.", + "cost": "medium" + }, { "id": "web_search", "label": "Search the web", @@ -24294,6 +29703,9 @@ "available_document_search": false, "available_document_compare": false, "available_tabular_analyze": false, + "available_tabular_inspect": false, + "available_tabular_merge": false, + "available_document_merge": false, "available_web_search": false, "available_url_fetch": false, "available_deep_research": false, @@ -24309,6 +29721,9 @@ "required_document_search": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_url_fetch": false, "required_deep_research": false, @@ -24357,6 +29772,9 @@ "available_document_search": false, "available_document_compare": false, "available_tabular_analyze": false, + "available_tabular_inspect": false, + "available_tabular_merge": false, + "available_document_merge": false, "available_web_search": false, "available_url_fetch": false, "available_deep_research": false, @@ -24372,6 +29790,9 @@ "required_document_search": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_url_fetch": false, "required_deep_research": false, @@ -24436,6 +29857,9 @@ "available_document_search": false, "available_document_compare": false, "available_tabular_analyze": false, + "available_tabular_inspect": false, + "available_tabular_merge": false, + "available_document_merge": false, "available_web_search": false, "available_url_fetch": false, "available_deep_research": false, @@ -24451,6 +29875,9 @@ "required_document_search": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_url_fetch": false, "required_deep_research": false, @@ -24492,6 +29919,9 @@ "document_search": "not_enabled_for_orchestration", "document_compare": "not_enabled_for_orchestration", "tabular_analyze": "not_enabled_for_orchestration", + "tabular_inspect": "not_enabled_for_orchestration", + "tabular_merge": "not_enabled_for_orchestration", + "document_merge": "not_enabled_for_orchestration", "web_search": "not_enabled_for_orchestration", "url_fetch": "not_enabled_for_orchestration", "deep_research": "not_enabled_for_orchestration", @@ -24913,6 +30343,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -24928,6 +30361,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -24976,6 +30412,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -24991,6 +30430,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -25055,6 +30497,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -25070,6 +30515,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -25109,6 +30557,9 @@ "document_analyze", "document_compare", "tabular_analyze", + "tabular_inspect", + "tabular_merge", + "document_merge", "web_search", "action_invoke", "agent_invoke", @@ -25155,6 +30606,27 @@ "summary": "Compute over CSV or Excel data rather than reading it as prose.", "cost": "medium" }, + { + "id": "tabular_inspect", + "label": "Inspect spreadsheets", + "role": "gather", + "summary": "Read the sheets, column headers, row counts and a few sample rows of CSV or Excel files, and how their columns line up.", + "cost": "low" + }, + { + "id": "tabular_merge", + "label": "Merge spreadsheets", + "role": "reason", + "summary": "Combine the rows of two or more CSV or Excel files into one table, exactly and without a model, lining up columns by name, alias or a prepared mapping.", + "cost": "medium" + }, + { + "id": "document_merge", + "label": "Merge documents", + "role": "reason", + "summary": "Combine two or more PDFs, Word documents or PowerPoint decks into one file of the same kind, or CSV and Excel files into one workbook with a sheet per file, exactly and without a model.", + "cost": "medium" + }, { "id": "web_search", "label": "Search the web", @@ -25582,6 +31054,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -25597,6 +31072,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -25645,6 +31123,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -25660,6 +31141,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -25724,6 +31208,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -25739,6 +31226,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -25778,6 +31268,9 @@ "document_analyze", "document_compare", "tabular_analyze", + "tabular_inspect", + "tabular_merge", + "document_merge", "web_search", "action_invoke", "agent_invoke", @@ -25824,6 +31317,27 @@ "summary": "Compute over CSV or Excel data rather than reading it as prose.", "cost": "medium" }, + { + "id": "tabular_inspect", + "label": "Inspect spreadsheets", + "role": "gather", + "summary": "Read the sheets, column headers, row counts and a few sample rows of CSV or Excel files, and how their columns line up.", + "cost": "low" + }, + { + "id": "tabular_merge", + "label": "Merge spreadsheets", + "role": "reason", + "summary": "Combine the rows of two or more CSV or Excel files into one table, exactly and without a model, lining up columns by name, alias or a prepared mapping.", + "cost": "medium" + }, + { + "id": "document_merge", + "label": "Merge documents", + "role": "reason", + "summary": "Combine two or more PDFs, Word documents or PowerPoint decks into one file of the same kind, or CSV and Excel files into one workbook with a sheet per file, exactly and without a model.", + "cost": "medium" + }, { "id": "web_search", "label": "Search the web", @@ -26025,6 +31539,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -26040,6 +31557,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -26088,6 +31608,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -26103,6 +31626,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -26167,6 +31693,9 @@ "available_document_analyze": true, "available_document_compare": true, "available_tabular_analyze": true, + "available_tabular_inspect": true, + "available_tabular_merge": true, + "available_document_merge": true, "available_web_search": true, "available_action_invoke": true, "available_agent_invoke": true, @@ -26182,6 +31711,9 @@ "required_document_analyze": false, "required_document_compare": false, "required_tabular_analyze": false, + "required_tabular_inspect": false, + "required_tabular_merge": false, + "required_document_merge": false, "required_web_search": false, "required_action_invoke": false, "required_agent_invoke": false, @@ -26221,6 +31753,9 @@ "document_analyze", "document_compare", "tabular_analyze", + "tabular_inspect", + "tabular_merge", + "document_merge", "web_search", "action_invoke", "agent_invoke", @@ -26267,6 +31802,27 @@ "summary": "Compute over CSV or Excel data rather than reading it as prose.", "cost": "medium" }, + { + "id": "tabular_inspect", + "label": "Inspect spreadsheets", + "role": "gather", + "summary": "Read the sheets, column headers, row counts and a few sample rows of CSV or Excel files, and how their columns line up.", + "cost": "low" + }, + { + "id": "tabular_merge", + "label": "Merge spreadsheets", + "role": "reason", + "summary": "Combine the rows of two or more CSV or Excel files into one table, exactly and without a model, lining up columns by name, alias or a prepared mapping.", + "cost": "medium" + }, + { + "id": "document_merge", + "label": "Merge documents", + "role": "reason", + "summary": "Combine two or more PDFs, Word documents or PowerPoint decks into one file of the same kind, or CSV and Excel files into one workbook with a sheet per file, exactly and without a model.", + "cost": "medium" + }, { "id": "web_search", "label": "Search the web", diff --git a/functional_tests/test_workflow_handoff_builder.py b/functional_tests/test_workflow_handoff_builder.py index b64e0e498..cfbb5d523 100644 --- a/functional_tests/test_workflow_handoff_builder.py +++ b/functional_tests/test_workflow_handoff_builder.py @@ -148,8 +148,11 @@ RECORDS_SCHEMA = {"type": "array", "items": {"type": "object"}} # The Phase 4 blueprint schema, payloads, validation and dry run, captured before hand-off existed. +# The schema digest was refreshed after #1641 (merged into Phase 7a at b8e47302b) added the merge +# task schema. It equals the value at V2 a5a5b1c53, before hand-off merged, so this test still +# proves hand-off left the Phase 4 schema unchanged. PHASE4_DIGESTS = { - "schema": "ab12fb1d29c7eaa028a534efa4f610795a31e25638643f30a8ec399b493d9b26", + "schema": "520139cbce120ca017106bf200832712913e395c6cd3e6fa9b530eae01cbd66e", "payload_email": "44aced826054039a54ae63588ef6f1a4b5d22447a80d26c2af0f7cfb891739fc", "payload_review": "1f93a59c2c6870eaad04a324b4b86262c0f0da6eea745b5a91835b7d0bb73531", "validate": "36537c8af7af0882ab7615b0e1014b5eca1181235af439c919ebf7692993197f", diff --git a/functional_tests/test_workflow_handoff_result_reader.py b/functional_tests/test_workflow_handoff_result_reader.py index 332d26ac9..d47bbbfa0 100644 --- a/functional_tests/test_workflow_handoff_result_reader.py +++ b/functional_tests/test_workflow_handoff_result_reader.py @@ -2,8 +2,8 @@ # test_workflow_handoff_result_reader.py """ Functional test for reading a one-time hand-off's report in the workflow result reader. -Version: 0.261.250 -Implemented in: 0.261.250 +Version: 0.261.252 +Implemented in: 0.261.250 (the report's lineage re-proof: 0.261.252) This test ensures that the result reader, which keeps structured (v3) runs closed, opens exactly one shape: a one-time chat hand-off run whose single workflow output is the report node's text. @@ -11,13 +11,17 @@ * The run must come from chat orchestration, its workflow must be the hand-off's one-time workflow under the deterministic id, and its one receipt must name the report node, its text output and a well-formed reference. Every other structured shape stays closed with no read. -* The report is read through its exact node selectors, its lineage is re-proved against the saved - flow, so an edited or re-enabled definition fails closed, and the result is bound to a digest. +* The report is read through its exact node selectors and bound to a digest. An edited or + re-enabled definition fails closed before any load. +* Before the result is described or excerpted, the shared node lineage authorizer re-proves the + report's lineage against the saved flow: every consumed-input receipt must chain to a real + parent result of this run. A malformed receipt, a missing parent or a corrupt parent closes + the read, and the report's text is never loaded. * Excerpts stay bounded, carry no store references or ids, and page a large report once. It uses fake containers and an in-memory canonical result store; the hand-off builder, the result -contract, the node identity and the reader are real. Checks use explicit raises, so they hold -under ``python -O``. +contract, the node identity, the lineage authorizer and the reader are real. Checks use explicit +raises, so they hold under ``python -O``. """ import json @@ -36,28 +40,41 @@ import functions_workflow_result_reader as reader # noqa: E402 from functions_workflow_execution import workflow_execution_scope # noqa: E402 from functions_workflow_handoff_builder import build_handoff_definition # noqa: E402 -from functions_workflow_identity import workflow_execution_id # noqa: E402 -from functions_workflow_result_store import MAX_PAGE_BYTES # noqa: E402 -from functions_workflow_results import build_workflow_task_result, persist_workflow_task_result # noqa: E402 +from functions_workflow_identity import workflow_execution_id, workflow_node_identity # noqa: E402 +from functions_workflow_node_results import open_workflow_record_input, result_selectors # noqa: E402 +# _verify_payload is the real store's size and digest check; _build_task_result is how the +# structured runner builds an engine node's (collect's) result, which has no task. +from functions_workflow_result_store import MAX_PAGE_BYTES, _verify_payload # noqa: E402 +from functions_workflow_results import ( # noqa: E402 + _build_task_result, + build_workflow_task_result, + persist_workflow_task_result, +) from test_support.versioning import assert_app_version_at_least # noqa: E402 from test_support.workflow_result_chat import ( # noqa: E402 USER, CanonicalStore, FakeContainer, + RunFixture, accepted_partial, + analysis_result, + canonical, closed, ) MINIMUM_VERSION = "0.261.250" +LINEAGE_VERSION = "0.261.252" HANDOFF_ID = "handoff-7f3" RUN = "run-handoff-41" CONVERSATION = "conversation-9" COMPLETED_AT = "2026-01-05T14:02:00+00:00" REQUESTED_AT = "2026-01-05T13:00:00+00:00" REPORT_TEXT = "The review found two contracts that need an owner." +COLLECTED = [{"finding": "Contract A needs an owner."}, {"finding": "Contract B needs an owner."}] WID = reader._handoff_workflow_id(USER, HANDOFF_ID) DEFAULT_BUDGET = reader.DEFAULT_EXCERPT_BUDGET_BYTES +UNAVAILABLE = "[WorkflowResults] Workflow result unavailable" def _require(condition, message): @@ -107,9 +124,14 @@ def read_page(self, workflow, run_id, task_id, reference, *, offset=0, limit=655 class HandoffFixture: - """A finished hand-off run whose report was saved through the real v3 result contract.""" + """A finished hand-off run whose report was saved through the real v3 result contract. + + ``lineage``, when given, saves the report's parents and returns its consumed-input receipts. It + gets only the saved workflow and the store, so no hook runs on a half-built fixture. Without it + the report consumed nothing, so there's no lineage to walk. + """ - def __init__(self, *, status="completed", title="Board summary", result=None, partial=False): + def __init__(self, *, status="completed", title="Board summary", result=None, partial=False, lineage=None): self.store = NodeStore() blueprint = { "name": "Contract review", @@ -146,6 +168,8 @@ def __init__(self, *, status="completed", title="Board summary", result=None, pa envelope["workflow_validation"] = {"version": 1, "status": "valid", "eligible": True} if partial: accepted_partial(envelope) + if lineage is not None: + envelope["consumed_inputs"] = lineage(workflow, self.store) self.manifest, self.reference = persist_workflow_task_result( envelope, workflow=workflow, run_id=RUN, task_id=self.task_id, save_result=self.store.save, ) @@ -199,6 +223,89 @@ def digest(self, status="completed"): }]) +class CollectedParent: + """The report's ``collect`` parent: a real records result, saved the way the runner saves it. + + The collect node is an engine node with no task, so its result is built the way the runner + builds a control node's result and saved through the real v2 contract. The report's receipt + is the one the runner records for a records input: the real record reader proves the parent + and names the exact output, and the runner adds the input name. ``tamper``, when given, may + then change the receipt; it gets the parent's manifest and a way to save another parent. + """ + + def __init__(self, tamper=None): + self.tamper = tamper + self.identity = self.manifest = self.reference = None + + def receipts(self, workflow, store): + identity = workflow_node_identity(workflow, RUN, "collect", workflow_execution_id(workflow, RUN, "collect"), 1) + collected = _build_task_result({ + "reply": "Collected two findings.", + "authoritative_result": {"kind": "records", "value": deepcopy(COLLECTED)}, + }, identity, "workflow-result-v2") + collected["consumed_inputs"] = [] + collected["workflow_validation"] = {"version": 1, "status": "valid", "eligible": True} + self.identity = identity + self.manifest, self.reference = persist_workflow_task_result( + collected, workflow=workflow, run_id=RUN, task_id=None, save_result=store.save, + ) + records = open_workflow_record_input( + workflow, RUN, identity, self.reference, output_name="records", + reader_user_id=USER, load_result=store.load, + ) + receipt = {**records.receipt, "input_name": "findings"} + if self.tamper is not None: + self.tamper(receipt, self.manifest, lambda parent: store.save(workflow, RUN, None, parent)) + return [receipt] + + +class LineageFixture(HandoffFixture): + """A hand-off whose report consumed a real ``collect`` result, through the runner's contract.""" + + def __init__(self, *, tamper=None, **options): + parent = CollectedParent(tamper) + super().__init__(lineage=parent.receipts, **options) + self.collect_identity = parent.identity + self.collect_manifest = parent.manifest + self.collect_reference = parent.reference + self.reset() + + +def _repoint(fixture, manifest): + """Save an edited report manifest under its own valid hash and point the run's receipt at it.""" + reference = fixture.store.save(fixture.workflow, RUN, fixture.task_id, manifest) + fixture.receipt["result_ref"] = reference + return reference + + +def _digest_for(fixture, reference, status="completed"): + return reader.workflow_result_digest(WID, RUN, status, [{ + "task_id": fixture.task_id, "workflow_result": {"authoritative_output": "text", "result_ref": reference}, + }]) + + +def _closed_stages(monkeypatch): + """Record the reader's closed-reason log entries, which name the stage that closed the read.""" + logged = [] + + def record(message, extra=None, **kwargs): + if message == UNAVAILABLE: + logged.append(dict(extra or {})) + + monkeypatch.setattr(reader, "log_event", record) + return logged + + +def _verifying(store): + """The canonical store's loader with the real result store's size and digest check.""" + def load(workflow, run_id, task_id, reference, **selectors): + value = store.load(workflow, run_id, task_id, reference, **selectors) + _verify_payload(store.contents[reference["sha256"]].encode("ascii"), reference) + return value + + return load + + def _refused(fixture, code, **options): error = closed(lambda: fixture.read(**options)) _same(error.code, code, "the closed reason") @@ -214,6 +321,10 @@ def test_version_is_at_least_the_handoff_release(): assert_app_version_at_least(MINIMUM_VERSION) +def test_version_is_at_least_the_lineage_release(): + assert_app_version_at_least(LINEAGE_VERSION) + + def test_the_handoff_workflow_id_matches_the_draft_service(): # functions_workflow_drafts.orchestration_workflow_id("owner-1", "handoff-1"), pinned by the builder test. _same(reader._handoff_workflow_id("owner-1", "handoff-1"), "481a936e-5e1d-5cc8-a4c9-d8165ded91bf", "the id") @@ -579,5 +690,216 @@ def test_a_chat_run_of_a_proposed_structured_workflow_stays_closed(): _unread(fixture, "a proposed workflow") +_READS = (("excerpts", {"include_excerpts": True}), ("descriptor", {})) +_LINEAGE_INVALID = {"code": "workflow_result_invalid", "stage": "authorize", "error_type": "AnalysisResultUnavailable"} + + +def test_a_malformed_consumed_input_receipt_is_refused_before_the_report_is_read(monkeypatch): + # Saved under its own valid hash, so the identity, output and completion checks all pass. + fixture = HandoffFixture() + manifest = deepcopy(fixture.manifest) + manifest["consumed_inputs"] = [{"malformed_receipt": True}] + reference = _repoint(fixture, manifest) + fixture.reset() + logged = _closed_stages(monkeypatch) + + for label, options in _READS: + _refused(fixture, "workflow_result_invalid", **options) + _same(fixture.store.loads, [reference["sha256"]], f"{label}: the loads") + _require(fixture.text_reference["sha256"] not in fixture.store.loads, f"{label}: the report's text was loaded.") + _same(fixture.store.pages, [], f"{label}: the page reads") + _same(logged, [_LINEAGE_INVALID], f"{label}: the closed stage") + fixture.reset() + logged.clear() + + context = {"workflow_id": WID, "run_id": RUN, "result_sha256": _digest_for(fixture, reference)} + error = closed(lambda: reader.authorize_workflow_result_context( + USER, context, containers=fixture.containers, load_result=fixture.store.load, read_page=fixture.store.read_page, + )) + _same(error.code, "workflow_result_invalid", "the stored context's closed reason") + _same(fixture.store.loads, [reference["sha256"]], "the stored context's loads") + _same(fixture.store.pages, [], "the stored context's page reads") + _same(logged, [_LINEAGE_INVALID], "the stored context's closed stage") + + # Without the walk the same manifest reads as a normal report, so the refusal is the walk's. + monkeypatch.setattr(reader, "authorize_workflow_node_result_read", lambda *args, **kwargs: None) + fixture.reset() + unwalked = fixture.read(include_excerpts=True) + _same(unwalked["descriptor"]["available"], True, "availability without the walk") + _same(unwalked["excerpts"][0]["text"], REPORT_TEXT, "the report text without the walk") + + +def test_a_report_with_real_lineage_reads_after_walking_its_parent(monkeypatch): + fixture = LineageFixture() + report, collect, text = ( + fixture.reference["sha256"], fixture.collect_reference["sha256"], fixture.text_reference["sha256"], + ) + _same(fixture.manifest["consumed_inputs"], [{ + "producer": fixture.collect_identity, "output_name": "records", "result_ref": fixture.collect_reference, + "output_ref": fixture.collect_manifest["outputs"]["records"]["result_ref"], "input_name": "findings", + }], "the report's consumed-input receipt") + _same(fixture.collect_identity["node_id"], "collect", "the parent node") + + result = fixture.read(include_excerpts=True) + + _same(result["descriptor"]["available"], True, "availability") + _same(result["descriptor"]["result_sha256"], fixture.digest(), "the digest") + _same(result["excerpts"], [{ + "label": "Board summary", "kind": "text", "final": True, "text": REPORT_TEXT, "truncated": False, "note": None, + }], "the excerpts") + _same(fixture.store.loads, [report, collect, text], "the loads: the manifest, its lineage, then the text section") + _same(fixture.store.pages, [], "the page reads") + _same(fixture.store.selectors, [ + ("load", report, fixture.selectors), + ("load", collect, result_selectors(fixture.collect_identity)), + ("load", text, fixture.selectors), + ], "the selectors") + + # Without the walk the result is identical and only the parent's load disappears, so this + # control passes by walking real lineage, not because there was nothing to walk. + calls = [] + + def skip_walk(workflow, run_id, identity, reference, **options): + calls.append({ + "run_id": run_id, "identity": identity, "reference": reference, + "reader_user_id": options.get("reader_user_id"), "manifest": options.get("manifest"), + }) + + monkeypatch.setattr(reader, "authorize_workflow_node_result_read", skip_walk) + fixture.reset() + _same(fixture.read(include_excerpts=True), result, "the result without the walk") + _same(fixture.store.loads, [report, text], "the loads without the walk") + _same(calls, [{ + "run_id": RUN, "identity": fixture.manifest["identity"], "reference": fixture.reference, + "reader_user_id": USER, "manifest": fixture.manifest, + }], "the walk's arguments") + + +def test_a_descriptor_alone_walks_the_lineage_but_reads_no_section(): + fixture = LineageFixture() + expected = [fixture.reference["sha256"], fixture.collect_reference["sha256"]] + + result = fixture.read() + + _same(result["excerpts"], [], "the excerpts") + _same(result["descriptor"]["available"], True, "availability") + _same(result["descriptor"]["result_sha256"], fixture.digest(), "the digest") + _same(fixture.store.loads, expected, "the loads") + _same(fixture.store.pages, [], "the page reads") + + fixture.reset() + descriptor = reader.authorize_workflow_result_context( + USER, reader.workflow_result_context(result["descriptor"]), containers=fixture.containers, + load_result=fixture.store.load, read_page=fixture.store.read_page, + ) + _same(descriptor, result["descriptor"], "the authorized descriptor") + _same(fixture.store.loads, expected, "the stored context's loads") + _same(fixture.store.pages, [], "the stored context's page reads") + + +def test_a_missing_parent_is_refused_with_the_general_paths_code(monkeypatch): + fixture = LineageFixture() + del fixture.store.contents[fixture.collect_reference["sha256"]] + logged = _closed_stages(monkeypatch) + missing = {"code": "workflow_result_not_found", "stage": "authorize", "error_type": "CosmosResourceNotFoundError"} + walked = [fixture.reference["sha256"], fixture.collect_reference["sha256"]] + + for label, options in _READS: + error = _refused(fixture, "workflow_result_not_found", **options) + _same(error.status, 404, f"{label}: the status") + _same(fixture.store.loads, walked, f"{label}: the loads") + _same(fixture.store.pages, [], f"{label}: the page reads") + _same(logged, [missing], f"{label}: the closed stage") + fixture.reset() + logged.clear() + + # The general path walks the same lineage for each task row. This report consumed a parent + # whose row and stored result are both gone, so only the walk can reach the missing parent. + general = RunFixture() + _, parent = general.add_task("task-collect-1", analysis_result(), order=1) + general.add_task("task-report-2", {"reply": REPORT_TEXT}, order=2, consumed=[ + general.receipt("task-collect-1", parent), + ]) + general.items[:] = [item for item in general.items if item["task_id"] != "task-collect-1"] + del general.store.contents[parent["sha256"]] + general.reset_counters() + + error = closed(lambda: general.read(include_excerpts=True)) + + _same(error.code, "workflow_result_not_found", "the general path's closed reason") + _same(error.status, 404, "the general path's status") + _same(general.store.loads[-1:], [parent["sha256"]], "the general path's last load, the missing parent") + _same(logged, [missing], "the general path's closed stage") + + +def _wrong_receipt_output(receipt, parent, save): + receipt["output_ref"] = {"sha256": "c" * 64, "size_bytes": 10} + + +def _wrong_parent_output(receipt, parent, save): + # A parent saved under its own valid hash whose records output names another section. + altered = deepcopy(parent) + altered["outputs"]["records"]["result_ref"] = deepcopy(altered["outputs"]["text"]["result_ref"]) + receipt["result_ref"] = save(altered) + + +@pytest.mark.parametrize("tamper", [_wrong_receipt_output, _wrong_parent_output], ids=["receipt", "parent"]) +def test_a_parent_whose_output_disagrees_with_the_receipt_is_refused(tamper, monkeypatch): + fixture = LineageFixture(tamper=tamper) + parent = fixture.manifest["consumed_inputs"][0]["result_ref"]["sha256"] + logged = _closed_stages(monkeypatch) + + for label, options in _READS: + _refused(fixture, "workflow_result_invalid", **options) + _same(fixture.store.loads, [fixture.reference["sha256"], parent], f"{label}: the loads") + _same(fixture.store.pages, [], f"{label}: the page reads") + _same(logged, [_LINEAGE_INVALID], f"{label}: the closed stage") + fixture.reset() + logged.clear() + + +def test_a_parent_whose_bytes_do_not_match_its_hash_is_refused(monkeypatch): + fixture = LineageFixture() + load = _verifying(fixture.store) + verified = fixture.read(include_excerpts=True, load_result=load) + _same(verified["excerpts"][0]["text"], REPORT_TEXT, "the verified read") + # Same length, different bytes: only the digest disagrees. + altered = deepcopy(fixture.collect_manifest) + altered["summary"] = altered["summary"].replace("two", "six") + _require(altered != fixture.collect_manifest, "The parent wasn't altered.") + _same(len(canonical(altered)), fixture.collect_reference["size_bytes"], "the altered parent's size") + fixture.store.contents[fixture.collect_reference["sha256"]] = canonical(altered) + fixture.reset() + logged = _closed_stages(monkeypatch) + walked = [fixture.reference["sha256"], fixture.collect_reference["sha256"]] + + for label, options in _READS: + _refused(fixture, "workflow_result_invalid", load_result=load, **options) + _same(fixture.store.loads, walked, f"{label}: the loads") + _same(fixture.store.pages, [], f"{label}: the page reads") + _same(logged, [{ + "code": "workflow_result_invalid", "stage": "authorize", "error_type": "WorkflowResultIntegrityError", + }], f"{label}: the closed stage") + fixture.reset() + logged.clear() + + +@pytest.mark.parametrize("variant", ["definition_edit", "enable_toggle", "alert_edit", "run_as"]) +def test_an_edited_or_re_enabled_definition_with_lineage_fails_closed(variant): + fixture = LineageFixture() + if variant == "definition_edit": + fixture.workflow["tasks"][1]["instructions"] = "Write a different report." + elif variant == "enable_toggle": + fixture.workflow["is_enabled"] = True + elif variant == "alert_edit": + fixture.workflow["alert_mode"] = "always" + else: + fixture.workflow["m365_run_as_user_id"] = "user-other-2" + + for label, options in _READS: + _refused(fixture, "workflow_result_invalid", **options) + _unread(fixture, f"{variant}, {label}") + + if __name__ == "__main__": raise SystemExit(pytest.main([__file__, "-q"]))