Skip to content

feat: add agent response projection and truncation - #33

Merged
chaim0m merged 1 commit into
mainfrom
codex/dci-agent-output
Aug 3, 2026
Merged

feat: add agent response projection and truncation#33
chaim0m merged 1 commit into
mainfrom
codex/dci-agent-output

Conversation

@chaim0m

@chaim0m chaim0m commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • add generic --fields and --exclude projection for list and get responses
  • recognize the real DCI rowCount list wrappers used by budgets, folders, labels, annotations, and anomalies
  • truncate long strings in agent mode with an explicit remaining-character marker
  • add --full and --no-truncate escape hatches
  • emit a definitive {count: 0, results: []} shape for an empty top-level list in agent mode
  • preserve pagination and wrapper metadata
  • document the response-shaping flags in the README and embedded skill

Part of #9 and #12. Terse field renames and endpoint-specific minimal schemas remain API-owned because the CLI passes through OpenAPI response fields.

Test methods

Automated validation run on this branch:

go test ./...
go vet ./...

Manual validation with an authenticated CLI:

go build -o /tmp/dci-pr33 .
/tmp/dci-pr33 --no-agent list-budgets --output table
DCI_AGENT_MODE=1 /tmp/dci-pr33 list-budgets --fields id,name --output json
DCI_AGENT_MODE=1 /tmp/dci-pr33 list-budgets --exclude description --output json
DCI_AGENT_MODE=1 /tmp/dci-pr33 list-budgets --full --output json

--fields should project each object inside the budgets wrapper while preserving rowCount; it must not return {}. --exclude should remove requested fields, and --full should restore long values. Unit fixtures cover the same real wrapper shape for folders, labels, annotations, and anomalies.

Could this break things?

Risk: medium. API requests and human-mode tables are unchanged. Agent-mode consumers may notice long strings becoming truncated by default, and scripts can intentionally request fewer fields. Consumers that require the complete original response should use --full or --no-truncate.

Example truncation marker:

{"description":{"value":"first 2000 characters...","_truncated":4821}}

@chaim0m
chaim0m marked this pull request as ready for review August 2, 2026 13:38
@chaim0m
chaim0m requested a review from apgiorgi as a code owner August 2, 2026 13:38
@chaim0m
chaim0m force-pushed the codex/dci-agent-output branch from f8be216 to edf23d6 Compare August 2, 2026 14:19
@chaim0m
chaim0m changed the base branch from codex/dci-agent-errors to main August 2, 2026 14:19
Comment thread output_contract.go
Comment thread output_contract.go Outdated
@chaim0m chaim0m self-assigned this Aug 3, 2026
@apgiorgi

apgiorgi commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Re-reviewed at 0abaebb. Built the branch and tested against the live API.

The --fields blocker is fixed. Adding rowCount to hasListMetadata (output_contract.go:149) resolves it. Before the fix, list-budgets --fields id,budgetName returned {} with exit 0 against a tenant that has 4 budgets. Now verified working on list-budgets, list-labels, list-anomalies, and returning proper {"folders":[],"rowCount":0} for genuinely empty ones. Human-mode table renders correctly too (it was a row of <nil>).

Three things still open.

1. --exclude strips wrapper metadata and applies at every depth

transformResponseObjects (output_contract.go:196) applies the exclusion set to every object at every nesting level, so the "preserve pagination and wrapper metadata" claim in the PR description doesn't hold yet. Verified live:

$ DCI_AGENT_MODE=1 dci list-budgets --exclude rowCount,amount --output json | jq -c 'keys, (.budgets[0].alertThresholds[0]|keys)'
["budgets"]
["percentage"]

rowCount is gone from the wrapper, and alertThresholds[].amount is gone from a nested object the user didn't ask about. --exclude pageToken or --exclude cursor would break pagination the same way.

2. Unknown field names are a silent no-op

projectObject (output_contract.go:166) keeps exact matches and returns whatever it found, with no diagnostic:

$ DCI_AGENT_MODE=1 dci list-budgets --fields nosuchfield --output json
{"budgets":[{},{},{},{}],"rowCount":4}

Same result for a case mismatch (--fields ID) or the wrong field name (--fields name — budgets use budgetName). An agent can't distinguish "no data" from "wrong field name," which is the failure mode the original {} bug had. Worth a usage error, or at minimum a stderr warning, when zero requested fields exist in the response.

3. I'd still cut the truncation half

No commit in this PR has touched it, so this stands from the first pass:

  • It changes the value's type. string{value, _truncated} (output_contract.go:224) breaks every non-LLM consumer: jq -r .description yields null, JSON-schema validation on type: string fails, typed unmarshal fails. The CLI stops conforming to the OpenAPI schema it ships.
  • It costs more tokens than it saves on the default format. In toon (the agent-mode default) the map serializes as an escaped JSON string inside the cell, so the retained 2000 runes come back backslash-escaped and brace-wrapped — strictly more tokens than the original string, plus the row is no longer scalar.
  • It silently deletes table columns. filterObjectColumns hides map-valued columns, so one long row turns the field into a map and the whole column disappears for every row, with a footer that says "Hidden columns (object values)" about a string field.
  • It buys nothing on real DCI data. The longest string in any list endpoint I sampled is 125 chars (anomalies[].resourceData[].resource_id), well under the 2000 default at output_contract.go:10. Where long strings do occur — report filter/attribution expressions, presigned invoice URLs, pagination cursors — they're exactly the values an agent has to round-trip verbatim.

If truncation stays, I'd make it opt-in (--truncate[=N]), keep the field a string, and put the marker in a sibling key rather than changing the type.

One design question worth settling before this grows

Field projection is a query concern. Doing it client-side means paying full transfer cost and then discarding — it saves agent tokens but nothing else. The PR description already concedes the terse-field-name half is API-owned. Worth a conversation with the API team about ?fields= before we build more of this in the CLI. Related: restish already has -f/--filter which does nested projection, so we should decide whether --fields is a friendlier alias for that or a parallel mechanism.

Also worth deciding: --fields a,b is top-level exact-match only. Agents will want --fields id,scope.name. Dotted paths or don't ship the flag.

@chaim0m
chaim0m force-pushed the codex/dci-agent-output branch from 0abaebb to e204c47 Compare August 3, 2026 16:29
@chaim0m
chaim0m merged commit 0c27f70 into main Aug 3, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants