Skip to content

[#870] add-tags-persona-to-phase-even#879

Closed
nrslib wants to merge 1 commit into
mainfrom
takt/870/add-tags-persona-to-phase-even
Closed

[#870] add-tags-persona-to-phase-even#879
nrslib wants to merge 1 commit into
mainfrom
takt/870/add-tags-persona-to-phase-even

Conversation

@nrslib

@nrslib nrslib commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Summary

概要

現在の .phase.jsonl(phase usage event)には stepstep_type しかなく、ステップの tagspersona が含まれていない。

集計・分析時に「review 系ステップだけ」「coder ペルソナのトークン消費」といったフィルタができるよう、これらのフィールドを追加する。

やること

  • UsageEventsSpanProcessor でスパン属性から tagspersona を取得し、phase usage event に書き出す
  • ワークフローエンジン側でステップ実行時のスパンに takt.step.tagstakt.step.persona 属性を設定する
  • tools/token-usage.sh の CSV 出力にも tagspersona カラムを追加する

追加フィールド

{
  ...既存フィールド,
  "tags": ["coding", "review"],
  "persona": "coder"
}

関連

Execution Report

Workflow takt-default completed successfully.

Closes #870

Summary by CodeRabbit

リリースノート

  • 新機能

    • ステップのタグとペルソナがフェーズ使用イベントに記録されるようになりました。
    • トークン使用量のCSV出力にペルソナとタグカラムが追加されました。メタデータがエクスポートデータに含まれます。
  • テスト

    • タグとペルソナの記録・処理・出力に関する包括的なテストが追加されました。

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

スパン属性 takt.step.tags / takt.step.personaworkflowSpans.ts のフェーズ・ジャッジステージスパンに付与し、phaseUsageEvent.ts で安全に抽出・バリデーションして PhaseUsageEventLogRecord に格納する。併せて tools/token-usage.sh の CSV 出力に personatags カラムを追加し、各レイヤーに対応するテストを追加する。

Changes

tags/persona をフェーズ使用イベントと CSV に追加

Layer / File(s) Summary
AttributeInput 型拡張とスパン属性への takt.step.tags 付与
src/core/workflow/observability/workflowSpans.ts
AttributeInput の値型に string[] を追加し、buildPhaseAttributesbuildJudgeStageAttributes の両方に takt.step.tagsparams.step.tags)を設定する。
PhaseUsageEventLogRecord への tags/persona フィールド追加とバリデーション
src/core/logging/phaseUsageEvent.ts
PhaseUsageEventLogRecordtags?: string[]persona?: string を追加し、getStringArray ヘルパーで非空・全要素非空文字列を検証し、buildRecord から条件付きで出力する。
token-usage.sh への persona/tags カラム追加
tools/token-usage.sh
step 集計レコードに personatags を保持し、CSV ヘッダーと出力行に personatags(`
workflowSpans テスト
src/__tests__/workflowSpans.test.ts
runWithPhaseSpanrecordJudgeStageSpan のスパンに takt.step.tags が付与されること、タグ未設定時は属性が存在しないことを3件で検証する。
phaseUsageEvent テスト
src/__tests__/phaseUsageEvent.test.ts
tags/persona の正常付与・不在・空配列・非配列・非文字列要素・空文字要素の各バリデーション条件を7件で網羅的に検証する。
UsageEventsSpanProcessor テスト
src/__tests__/usageEventsSpanProcessor.test.ts
タグ/ペルソナ属性を持つフェーズスパンを onEnd に渡したとき出力 JSONL レコードに tagspersona が格納されることを検証するテストとヘルパーを追加する。
tokenUsageCsv テスト
src/__tests__/tokenUsageCsv.test.ts
JSONL フィクスチャを実ファイルで生成し spawnSync でスクリプトを実行して、CSV の persona/tags 列・first-wins・空値時の振る舞いを3件で検証する。

推定コードレビュー工数

🎯 3 (Moderate) | ⏱️ ~20 minutes

関連する可能性のある PR

  • nrslib/takt#745: workflowSpans.ts および関連テストを共通して変更しており、ステップスパン属性の拡張基盤を構築している。
  • nrslib/takt#785: phaseUsageEvent.ts と phase usage events の出力経路を導入したPRであり、本PRはその経路に tags/persona フィールドを追加している。
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR タイトルは「add-tags-persona-to-phase-even」で、主な変更内容である phase usage event への tags と persona フィールド追加を明確に表現しています。
Linked Issues check ✅ Passed PR は issue #870 のすべての要件を満たしており、UsageEventsSpanProcessor への tags/persona 抽出実装、ワークフローエンジンの属性設定、token-usage.sh の CSV 出力拡張が完了しています。
Out of Scope Changes check ✅ Passed すべての変更は issue #870 で指定された phase usage event への tags と persona フィールド追加に直接関連しており、スコープ外の変更は検出されません。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch takt/870/add-tags-persona-to-phase-even

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/__tests__/phaseUsageEvent.test.ts`:
- Around line 283-362: The test coverage for takt.step.tags validation is
incomplete and missing boundary cases for null and object shapes. Add two
additional test cases following the same pattern as the existing tests: one test
where takt.step.tags is set to null and another where it is set to an object
shape (instead of an array), and verify in both cases that the
mapSpanEndToPhaseUsageEvent function ignores these malformed values by asserting
the record does not have a tags property. This ensures the parser properly
handles unexpected value types at the config/parse boundary.

In `@src/__tests__/tokenUsageCsv.test.ts`:
- Line 1: The test file tokenUsageCsv.test.ts uses camelCase naming instead of
the required kebab-case convention specified in the coding guidelines. Rename
the file from tokenUsageCsv.test.ts to token-usage-csv.test.ts to follow the
kebab-case naming standard for TypeScript files.

In `@src/__tests__/workflowSpans.test.ts`:
- Around line 1023-1047: The test for the judge_stage span path only validates
the positive case where step.tags are provided. Add a negative test case
following the same pattern to verify that when step.tags are not set on the step
passed to recordJudgeStageSpan, the takt.step.tags attribute is not attached to
the judge span. This ensures consistency with the contract testing approach used
in runWithPhaseSpan tests and prevents contract misalignment between branches.

In `@tools/token-usage.sh`:
- Around line 160-163: The CSV output is missing proper cell escaping which
causes column misalignment when values contain commas, quotes, or newlines. The
code directly joins values with commas without escaping special characters that
may appear in the user-provided persona and tags fields. Before joining the
array in the console.log statement within the nested loop iterating through
run.steps, apply RFC 4180 CSV escaping to each value by wrapping values
containing commas, quotes, or newlines in double quotes, and escaping any
internal quotes by doubling them.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f0c42266-a2ee-4c2f-93b1-8d4a291a2de5

📥 Commits

Reviewing files that changed from the base of the PR and between 16026d4 and f0b1c60.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json, !package-lock.json
📒 Files selected for processing (7)
  • src/__tests__/phaseUsageEvent.test.ts
  • src/__tests__/tokenUsageCsv.test.ts
  • src/__tests__/usageEventsSpanProcessor.test.ts
  • src/__tests__/workflowSpans.test.ts
  • src/core/logging/phaseUsageEvent.ts
  • src/core/workflow/observability/workflowSpans.ts
  • tools/token-usage.sh

Comment on lines +283 to +362
it('ignores a non-array tags attribute', () => {
// Given a phase span whose tags attribute is malformed (not an array)
const span: SpanSnapshot = {
name: 'phase.implement.execute',
endTime: [1_778_777_205, 0],
attributes: {
'takt.provider.name': 'codex',
'takt.model.name': 'gpt-5',
'takt.step.name': 'implement',
'takt.step.type': 'agent',
'takt.step.tags': 'coding',
'takt.phase.number': 1,
'takt.phase.name': 'execute',
'takt.phase.status': 'done',
'gen_ai.usage.input_tokens': 11,
'gen_ai.usage.output_tokens': 7,
'gen_ai.usage.total_tokens': 18,
},
};

// When the span is mapped to a phase usage event record
const record = mapSpanEndToPhaseUsageEvent(span, context);

// Then the malformed value is rejected rather than passed through
expect(record).not.toHaveProperty('tags');
});

it('ignores a tags array containing a non-string element', () => {
// Given a phase span whose tags array carries a non-string element
const span: SpanSnapshot = {
name: 'phase.implement.execute',
endTime: [1_778_777_205, 0],
attributes: {
'takt.provider.name': 'codex',
'takt.model.name': 'gpt-5',
'takt.step.name': 'implement',
'takt.step.type': 'agent',
'takt.step.tags': ['coding', 42],
'takt.phase.number': 1,
'takt.phase.name': 'execute',
'takt.phase.status': 'done',
'gen_ai.usage.input_tokens': 11,
'gen_ai.usage.output_tokens': 7,
'gen_ai.usage.total_tokens': 18,
},
};

// When the span is mapped to a phase usage event record
const record = mapSpanEndToPhaseUsageEvent(span, context);

// Then the whole array is rejected rather than partially emitted
expect(record).not.toHaveProperty('tags');
});

it('ignores a tags array containing an empty string element', () => {
// Given a phase span whose tags array carries an empty string element
const span: SpanSnapshot = {
name: 'phase.implement.execute',
endTime: [1_778_777_205, 0],
attributes: {
'takt.provider.name': 'codex',
'takt.model.name': 'gpt-5',
'takt.step.name': 'implement',
'takt.step.type': 'agent',
'takt.step.tags': ['coding', ''],
'takt.phase.number': 1,
'takt.phase.name': 'execute',
'takt.phase.status': 'done',
'gen_ai.usage.input_tokens': 11,
'gen_ai.usage.output_tokens': 7,
'gen_ai.usage.total_tokens': 18,
},
};

// When the span is mapped to a phase usage event record
const record = mapSpanEndToPhaseUsageEvent(span, context);

// Then the whole array is rejected rather than partially emitted
expect(record).not.toHaveProperty('tags');
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

takt.step.tags の境界値として null / object 形状も追加してください。

今回の負例は十分に近いですが、ガイドライン指定の null / object 形状が未カバーです。パーサ境界の契約回帰を防ぐため、この2ケースも tags 非出力を明示的に固定するのが安全です。

✅ 追加イメージ(例)
+  it('ignores a null tags attribute', () => {
+    const span: SpanSnapshot = {
+      name: 'phase.implement.execute',
+      endTime: [1_778_777_205, 0],
+      attributes: {
+        'takt.provider.name': 'codex',
+        'takt.step.name': 'implement',
+        'takt.step.type': 'agent',
+        'takt.step.tags': null as unknown as string[],
+        'takt.phase.number': 1,
+        'takt.phase.name': 'execute',
+        'takt.phase.status': 'done',
+        'gen_ai.usage.input_tokens': 1,
+        'gen_ai.usage.output_tokens': 1,
+        'gen_ai.usage.total_tokens': 2,
+      },
+    };
+    const record = mapSpanEndToPhaseUsageEvent(span, context);
+    expect(record).not.toHaveProperty('tags');
+  });
+
+  it('ignores an object-shaped tags attribute', () => {
+    const span: SpanSnapshot = {
+      name: 'phase.implement.execute',
+      endTime: [1_778_777_205, 0],
+      attributes: {
+        'takt.provider.name': 'codex',
+        'takt.step.name': 'implement',
+        'takt.step.type': 'agent',
+        'takt.step.tags': ({ tag: 'coding' } as unknown as string[]),
+        'takt.phase.number': 1,
+        'takt.phase.name': 'execute',
+        'takt.phase.status': 'done',
+        'gen_ai.usage.input_tokens': 1,
+        'gen_ai.usage.output_tokens': 1,
+        'gen_ai.usage.total_tokens': 2,
+      },
+    };
+    const record = mapSpanEndToPhaseUsageEvent(span, context);
+    expect(record).not.toHaveProperty('tags');
+  });

As per coding guidelines, "At config/parse boundaries, cover unexpected shapes (missing values, null/object instead of array) to confirm normalization/ignore behavior."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/__tests__/phaseUsageEvent.test.ts` around lines 283 - 362, The test
coverage for takt.step.tags validation is incomplete and missing boundary cases
for null and object shapes. Add two additional test cases following the same
pattern as the existing tests: one test where takt.step.tags is set to null and
another where it is set to an object shape (instead of an array), and verify in
both cases that the mapSpanEndToPhaseUsageEvent function ignores these malformed
values by asserting the record does not have a tags property. This ensures the
parser properly handles unexpected value types at the config/parse boundary.

Source: Coding guidelines

@@ -0,0 +1,96 @@
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

ファイル名は kebab-case に従う必要があります。

**/*.ts ファイルのコーディングガイドラインでは "Use kebab-case for filenames with focused module names" と明記されています。現在の tokenUsageCsv.test.ts は camelCase であり、token-usage-csv.test.ts に変更してください。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/__tests__/tokenUsageCsv.test.ts` at line 1, The test file
tokenUsageCsv.test.ts uses camelCase naming instead of the required kebab-case
convention specified in the coding guidelines. Rename the file from
tokenUsageCsv.test.ts to token-usage-csv.test.ts to follow the kebab-case naming
standard for TypeScript files.

Source: Coding guidelines

Comment on lines +1023 to +1047
it('sets step tags on judge stage spans for phase usage event filtering', async () => {
const { module, spans } = await loadWorkflowSpansWithMockedApi();
const step = makeStep({ personaDisplayName: 'conductor', tags: ['review'] });

module.recordJudgeStageSpan({
enabled: true,
workflowName: 'test-workflow',
step,
iteration: 1,
phaseExecutionId: 'implement:3:1:1',
entry: {
stage: 1,
method: 'structured_output',
status: 'done',
instruction: 'judge instruction',
response: 'judge response',
},
});

const judgeSpan = findSpan(spans, 'judge_stage.implement.1.structured_output');
expect(judgeSpan.attributes).toMatchObject({
'takt.step.persona': 'conductor',
'takt.step.tags': ['review'],
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

judge_stage 経路でも tags 未設定時の非出力契約を追加してください。

現状は judge_stage.* の正例のみで、step.tags が無い場合に takt.step.tags が付与されないことが未検証です。runWithPhaseSpan と同様の負例を1件追加して、分岐間の契約ずれを防いでください。

✅ 追加イメージ(例)
+  it('omits step tags on judge stage spans when the step has no tags', async () => {
+    const { module, spans } = await loadWorkflowSpansWithMockedApi();
+    const step = makeStep({ personaDisplayName: 'conductor' });
+
+    module.recordJudgeStageSpan({
+      enabled: true,
+      workflowName: 'test-workflow',
+      step,
+      iteration: 1,
+      phaseExecutionId: 'implement:3:1:1',
+      entry: {
+        stage: 1,
+        method: 'structured_output',
+        status: 'done',
+        instruction: 'judge instruction',
+        response: 'judge response',
+      },
+    });
+
+    const judgeSpan = findSpan(spans, 'judge_stage.implement.1.structured_output');
+    expect(judgeSpan.attributes).not.toHaveProperty('takt.step.tags');
+  });

As per coding guidelines, "When contract is consolidated/standardized ... verify existing equivalent branches still satisfy the new contract—not only the newly added path."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/__tests__/workflowSpans.test.ts` around lines 1023 - 1047, The test for
the judge_stage span path only validates the positive case where step.tags are
provided. Add a negative test case following the same pattern to verify that
when step.tags are not set on the step passed to recordJudgeStageSpan, the
takt.step.tags attribute is not attached to the judge span. This ensures
consistency with the contract testing approach used in runWithPhaseSpan tests
and prevents contract misalignment between branches.

Source: Coding guidelines

Comment thread tools/token-usage.sh
Comment on lines +160 to +163
console.log("task,run_id,provider,model,step,persona,tags,input_tokens,output_tokens,total_tokens,cached_tokens,calls");
for (const run of runs.slice(0, top)) {
for (const [step, s] of [...run.steps.entries()].sort((a, b) => b[1].total - a[1].total)) {
console.log([run.task || "-", run.run_id, run.provider, run.model, step, s.input, s.output, s.total, s.cached, s.count].join(","));
console.log([run.task || "-", run.run_id, run.provider, run.model, step, s.persona || "", s.tags.join("|"), s.input, s.output, s.total, s.cached, s.count].join(","));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

CSVセルのエスケープがなく、persona/tags で列崩れします

join(",") のみだと、値に , / " / 改行が含まれた時点でCSV契約が壊れます。今回追加した personatags は自由入力由来なので、RFC 4180 相当のエスケープを入れてから連結してください。

差分案
+function csvCell(v) {
+  const s = String(v ?? "");
+  return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
+}
+
 if (csv) {
   console.log("task,run_id,provider,model,step,persona,tags,input_tokens,output_tokens,total_tokens,cached_tokens,calls");
   for (const run of runs.slice(0, top)) {
     for (const [step, s] of [...run.steps.entries()].sort((a, b) => b[1].total - a[1].total)) {
-      console.log([run.task || "-", run.run_id, run.provider, run.model, step, s.persona || "", s.tags.join("|"), s.input, s.output, s.total, s.cached, s.count].join(","));
+      console.log([
+        run.task || "-",
+        run.run_id,
+        run.provider,
+        run.model,
+        step,
+        s.persona || "",
+        s.tags.join("|"),
+        s.input,
+        s.output,
+        s.total,
+        s.cached,
+        s.count,
+      ].map(csvCell).join(","));
     }
   }
   process.exit(0);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/token-usage.sh` around lines 160 - 163, The CSV output is missing
proper cell escaping which causes column misalignment when values contain
commas, quotes, or newlines. The code directly joins values with commas without
escaping special characters that may appear in the user-provided persona and
tags fields. Before joining the array in the console.log statement within the
nested loop iterating through run.steps, apply RFC 4180 CSV escaping to each
value by wrapping values containing commas, quotes, or newlines in double
quotes, and escaping any internal quotes by doubling them.

@nrslib

nrslib commented Jul 16, 2026

Copy link
Copy Markdown
Owner Author

Superseded by #1033, which incorporates the preferred implementation and additional CSV grouping, escaping, validation, and documentation improvements.

@nrslib nrslib closed this Jul 16, 2026
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.

phase usage event に tags と persona を追加する

1 participant